From 9d4e43cf55bb4eb474dbf62793b1266829d3d3b6 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 10 Jul 2024 04:59:30 -0700 Subject: [PATCH] Support multiple DataSource configurations in DataSourceContractTest PiperOrigin-RevId: 650967939 --- .../cronet/CronetDataSourceContractTest.java | 8 +- .../test/utils/DataSourceContractTest.java | 853 +++++++++--------- 2 files changed, 435 insertions(+), 426 deletions(-) diff --git a/libraries/datasource_cronet/src/androidTest/java/androidx/media3/datasource/cronet/CronetDataSourceContractTest.java b/libraries/datasource_cronet/src/androidTest/java/androidx/media3/datasource/cronet/CronetDataSourceContractTest.java index 62da3a9027..a9331f8039 100644 --- a/libraries/datasource_cronet/src/androidTest/java/androidx/media3/datasource/cronet/CronetDataSourceContractTest.java +++ b/libraries/datasource_cronet/src/androidTest/java/androidx/media3/datasource/cronet/CronetDataSourceContractTest.java @@ -25,6 +25,7 @@ import androidx.media3.test.utils.HttpDataSourceTestEnv; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; +import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.chromium.net.CronetEngine; @@ -45,15 +46,16 @@ public class CronetDataSourceContractTest extends DataSourceContractTest { } @Override - protected DataSource createDataSource() { + protected List createDataSources() { @Nullable CronetEngine cronetEngine = CronetUtil.buildCronetEngine( ApplicationProvider.getApplicationContext(), /* userAgent= */ "test-agent", - /* preferGMSCoreCronet= */ false); + /* preferGooglePlayServices= */ false); assertThat(cronetEngine).isNotNull(); - return new CronetDataSource.Factory(cronetEngine, executorService).createDataSource(); + return ImmutableList.of( + new CronetDataSource.Factory(cronetEngine, executorService).createDataSource()); } @Override diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/DataSourceContractTest.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/DataSourceContractTest.java index c9db9dc5cb..b43a8ff00a 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/DataSourceContractTest.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/DataSourceContractTest.java @@ -17,6 +17,7 @@ package androidx.media3.test.utils; import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Util.castNonNull; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -58,9 +59,10 @@ import org.mockito.Mockito; /** * A collection of contract tests for {@link DataSource} implementations. * - *

Subclasses should only include the logic necessary to construct the DataSource and allow it to - * successfully read data. They shouldn't include any new {@link Test @Test} methods - - * implementation-specific tests should be in a separate class. + *

Subclasses should only include the logic necessary to construct the DataSource (overriding + * either {@link #createDataSource()} or {@link #createDataSources()}) and allow it to successfully + * read data (overriding {@link #getTestResources()}. They shouldn't include any new {@link + * Test @Test} methods - implementation-specific tests should be in a separate class. * *

Most implementations should pass all these tests. If necessary, subclasses can disable tests * by overriding the {@link Test @Test} method with a no-op implementation. It's recommended (but @@ -72,8 +74,25 @@ public abstract class DataSourceContractTest { @Rule public final AdditionalFailureInfo additionalFailureInfo = new AdditionalFailureInfo(); - /** Creates and returns an instance of the {@link DataSource}. */ - protected abstract DataSource createDataSource() throws Exception; + /** + * Creates and returns an instance of the {@link DataSource}. + * + *

Only one of {@link #createDataSource()} and {@link #createDataSources()} should be + * implemented. + */ + protected DataSource createDataSource() throws Exception { + throw new UnsupportedOperationException(); + } + + /** + * Creates and returns a list of instances of the {@link DataSource}. + * + *

Only one of {@link #createDataSource()} and {@link #createDataSources()} should be + * implemented. + */ + protected List createDataSources() throws Exception { + throw new UnsupportedOperationException(); + } /** * Returns the {@link DataSource} that will be included in the {@link TransferListener} callbacks @@ -113,238 +132,196 @@ public abstract class DataSourceContractTest { @Test public void unboundedDataSpec_readUntilEnd() throws Exception { - ImmutableList resources = getTestResources(); - Assertions.checkArgument(!resources.isEmpty(), "Must provide at least one test resource."); + forAllTestResourcesAndDataSources( + (resource, dataSource) -> { + try { + long length = dataSource.open(new DataSpec(resource.getUri())); + byte[] data = + unboundedReadsAreIndefinite() + ? DataSourceUtil.readExactly(dataSource, resource.getExpectedBytes().length) + : DataSourceUtil.readToEnd(dataSource); - for (int i = 0; i < resources.size(); i++) { - additionalFailureInfo.setInfo(getFailureLabel(resources, i)); - TestResource resource = resources.get(i); - DataSource dataSource = createDataSource(); - try { - long length = dataSource.open(new DataSpec(resource.getUri())); - byte[] data = - unboundedReadsAreIndefinite() - ? DataSourceUtil.readExactly(dataSource, resource.getExpectedBytes().length) - : DataSourceUtil.readToEnd(dataSource); - - if (length != C.LENGTH_UNSET) { - assertThat(length).isEqualTo(resource.getExpectedBytes().length); - } - assertThat(data).isEqualTo(resource.getExpectedBytes()); - } finally { - dataSource.close(); - } - additionalFailureInfo.setInfo(null); - } + if (length != C.LENGTH_UNSET) { + assertThat(length).isEqualTo(resource.getExpectedBytes().length); + } + assertThat(data).isEqualTo(resource.getExpectedBytes()); + } finally { + dataSource.close(); + } + }); } @Test public void dataSpecWithPosition_readUntilEnd() throws Exception { - ImmutableList resources = getTestResources(); - Assertions.checkArgument(!resources.isEmpty(), "Must provide at least one test resource."); + forAllTestResourcesAndDataSources( + (resource, dataSource) -> { + try { + long length = + dataSource.open( + new DataSpec.Builder().setUri(resource.getUri()).setPosition(3).build()); + byte[] data = + unboundedReadsAreIndefinite() + ? DataSourceUtil.readExactly(dataSource, resource.getExpectedBytes().length - 3) + : DataSourceUtil.readToEnd(dataSource); - for (int i = 0; i < resources.size(); i++) { - additionalFailureInfo.setInfo(getFailureLabel(resources, i)); - TestResource resource = resources.get(i); - DataSource dataSource = createDataSource(); - try { - long length = - dataSource.open( - new DataSpec.Builder().setUri(resource.getUri()).setPosition(3).build()); - byte[] data = - unboundedReadsAreIndefinite() - ? DataSourceUtil.readExactly(dataSource, resource.getExpectedBytes().length - 3) - : DataSourceUtil.readToEnd(dataSource); - - if (length != C.LENGTH_UNSET) { - assertThat(length).isEqualTo(resource.getExpectedBytes().length - 3); - } - byte[] expectedData = - Arrays.copyOfRange(resource.getExpectedBytes(), 3, resource.getExpectedBytes().length); - assertThat(data).isEqualTo(expectedData); - } finally { - dataSource.close(); - } - additionalFailureInfo.setInfo(null); - } + if (length != C.LENGTH_UNSET) { + assertThat(length).isEqualTo(resource.getExpectedBytes().length - 3); + } + byte[] expectedData = + Arrays.copyOfRange( + resource.getExpectedBytes(), 3, resource.getExpectedBytes().length); + assertThat(data).isEqualTo(expectedData); + } finally { + dataSource.close(); + } + }); } @Test public void dataSpecWithLength_readExpectedRange() throws Exception { - ImmutableList resources = getTestResources(); - Assertions.checkArgument(!resources.isEmpty(), "Must provide at least one test resource."); + forAllTestResourcesAndDataSources( + (resource, dataSource) -> { + try { + long length = + dataSource.open( + new DataSpec.Builder().setUri(resource.getUri()).setLength(4).build()); + byte[] data = DataSourceUtil.readToEnd(dataSource); - for (int i = 0; i < resources.size(); i++) { - additionalFailureInfo.setInfo(getFailureLabel(resources, i)); - TestResource resource = resources.get(i); - DataSource dataSource = createDataSource(); - try { - long length = - dataSource.open(new DataSpec.Builder().setUri(resource.getUri()).setLength(4).build()); - byte[] data = DataSourceUtil.readToEnd(dataSource); - - assertThat(length).isEqualTo(4); - byte[] expectedData = Arrays.copyOf(resource.getExpectedBytes(), 4); - assertThat(data).isEqualTo(expectedData); - } finally { - dataSource.close(); - } - additionalFailureInfo.setInfo(null); - } + assertThat(length).isEqualTo(4); + byte[] expectedData = Arrays.copyOf(resource.getExpectedBytes(), 4); + assertThat(data).isEqualTo(expectedData); + } finally { + dataSource.close(); + } + }); } @Test public void dataSpecWithPositionAndLength_readExpectedRange() throws Exception { - ImmutableList resources = getTestResources(); - Assertions.checkArgument(!resources.isEmpty(), "Must provide at least one test resource."); + forAllTestResourcesAndDataSources( + (resource, dataSource) -> { + try { + long length = + dataSource.open( + new DataSpec.Builder() + .setUri(resource.getUri()) + .setPosition(2) + .setLength(2) + .build()); + byte[] data = DataSourceUtil.readToEnd(dataSource); - for (int i = 0; i < resources.size(); i++) { - additionalFailureInfo.setInfo(getFailureLabel(resources, i)); - TestResource resource = resources.get(i); - DataSource dataSource = createDataSource(); - try { - long length = - dataSource.open( - new DataSpec.Builder() - .setUri(resource.getUri()) - .setPosition(2) - .setLength(2) - .build()); - byte[] data = DataSourceUtil.readToEnd(dataSource); - - assertThat(length).isEqualTo(2); - byte[] expectedData = Arrays.copyOfRange(resource.getExpectedBytes(), 2, 4); - assertThat(data).isEqualTo(expectedData); - } finally { - dataSource.close(); - } - additionalFailureInfo.setInfo(null); - } + assertThat(length).isEqualTo(2); + byte[] expectedData = Arrays.copyOfRange(resource.getExpectedBytes(), 2, 4); + assertThat(data).isEqualTo(expectedData); + } finally { + dataSource.close(); + } + }); } @Test public void dataSpecWithPositionAtEnd_readsZeroBytes() throws Exception { - ImmutableList resources = getTestResources(); - Assertions.checkArgument(!resources.isEmpty(), "Must provide at least one test resource."); + forAllTestResourcesAndDataSources( + (resource, dataSource) -> { + int resourceLength = resource.getExpectedBytes().length; + DataSpec dataSpec = + new DataSpec.Builder().setUri(resource.getUri()).setPosition(resourceLength).build(); + try { + long length = dataSource.open(dataSpec); + byte[] data = + unboundedReadsAreIndefinite() + ? Util.EMPTY_BYTE_ARRAY + : DataSourceUtil.readToEnd(dataSource); - for (int i = 0; i < resources.size(); i++) { - additionalFailureInfo.setInfo(getFailureLabel(resources, i)); - TestResource resource = resources.get(i); - int resourceLength = resource.getExpectedBytes().length; - DataSource dataSource = createDataSource(); - DataSpec dataSpec = - new DataSpec.Builder().setUri(resource.getUri()).setPosition(resourceLength).build(); - try { - long length = dataSource.open(dataSpec); - byte[] data = - unboundedReadsAreIndefinite() - ? Util.EMPTY_BYTE_ARRAY - : DataSourceUtil.readToEnd(dataSource); - - // The DataSource.open() contract requires the returned length to equal the length in the - // DataSpec if set. This is true even though the DataSource implementation may know that - // fewer bytes will be read in this case. - if (length != C.LENGTH_UNSET) { - assertThat(length).isEqualTo(0); - } - assertThat(data).isEmpty(); - } finally { - dataSource.close(); - } - additionalFailureInfo.setInfo(null); - } + // The DataSource.open() contract requires the returned length to equal the length in + // the DataSpec if set. This is true even though the DataSource implementation may know + // that fewer bytes will be read in this case. + if (length != C.LENGTH_UNSET) { + assertThat(length).isEqualTo(0); + } + assertThat(data).isEmpty(); + } finally { + dataSource.close(); + } + }); } @Test public void dataSpecWithPositionAtEndAndLength_readsZeroBytes() throws Exception { - ImmutableList resources = getTestResources(); - Assertions.checkArgument(!resources.isEmpty(), "Must provide at least one test resource."); + forAllTestResourcesAndDataSources( + (resource, dataSource) -> { + int resourceLength = resource.getExpectedBytes().length; + DataSpec dataSpec = + new DataSpec.Builder() + .setUri(resource.getUri()) + .setPosition(resourceLength) + .setLength(1) + .build(); + try { + long length = dataSource.open(dataSpec); + byte[] data = + unboundedReadsAreIndefinite() + ? Util.EMPTY_BYTE_ARRAY + : DataSourceUtil.readToEnd(dataSource); - for (int i = 0; i < resources.size(); i++) { - additionalFailureInfo.setInfo(getFailureLabel(resources, i)); - TestResource resource = resources.get(i); - int resourceLength = resource.getExpectedBytes().length; - DataSource dataSource = createDataSource(); - DataSpec dataSpec = - new DataSpec.Builder() - .setUri(resource.getUri()) - .setPosition(resourceLength) - .setLength(1) - .build(); - try { - long length = dataSource.open(dataSpec); - byte[] data = - unboundedReadsAreIndefinite() - ? Util.EMPTY_BYTE_ARRAY - : DataSourceUtil.readToEnd(dataSource); - - // The DataSource.open() contract requires the returned length to equal the length in the - // DataSpec if set. This is true even though the DataSource implementation may know that - // fewer bytes will be read in this case. - assertThat(length).isEqualTo(1); - assertThat(data).isEmpty(); - } finally { - dataSource.close(); - } - additionalFailureInfo.setInfo(null); - } + // The DataSource.open() contract requires the returned length to equal the length in + // the DataSpec if set. This is true even though the DataSource implementation may know + // that fewer bytes will be read in this case. + assertThat(length).isEqualTo(1); + assertThat(data).isEmpty(); + } finally { + dataSource.close(); + } + }); } @Test public void dataSpecWithPositionOutOfRange_throwsPositionOutOfRangeException() throws Exception { - ImmutableList resources = getTestResources(); - Assertions.checkArgument(!resources.isEmpty(), "Must provide at least one test resource."); - - for (int i = 0; i < resources.size(); i++) { - additionalFailureInfo.setInfo(getFailureLabel(resources, i)); - TestResource resource = resources.get(i); - int resourceLength = resource.getExpectedBytes().length; - DataSource dataSource = createDataSource(); - DataSpec dataSpec = - new DataSpec.Builder().setUri(resource.getUri()).setPosition(resourceLength + 1).build(); - try { - IOException exception = assertThrows(IOException.class, () -> dataSource.open(dataSpec)); - assertThat(DataSourceException.isCausedByPositionOutOfRange(exception)).isTrue(); - } finally { - dataSource.close(); - } - additionalFailureInfo.setInfo(null); - } + forAllTestResourcesAndDataSources( + (resource, dataSource) -> { + int resourceLength = resource.getExpectedBytes().length; + DataSpec dataSpec = + new DataSpec.Builder() + .setUri(resource.getUri()) + .setPosition(resourceLength + 1) + .build(); + try { + IOException exception = + assertThrows(IOException.class, () -> dataSource.open(dataSpec)); + assertThat(DataSourceException.isCausedByPositionOutOfRange(exception)).isTrue(); + } finally { + dataSource.close(); + } + }); } @Test public void dataSpecWithEndPositionOutOfRange_readsToEnd() throws Exception { - ImmutableList resources = getTestResources(); - Assertions.checkArgument(!resources.isEmpty(), "Must provide at least one test resource."); + forAllTestResourcesAndDataSources( + (resource, dataSource) -> { + int resourceLength = resource.getExpectedBytes().length; + DataSpec dataSpec = + new DataSpec.Builder() + .setUri(resource.getUri()) + .setPosition(resourceLength - 1) + .setLength(2) + .build(); + try { + long length = dataSource.open(dataSpec); + byte[] data = DataSourceUtil.readExactly(dataSource, /* length= */ 1); + // TODO: Decide what the allowed behavior should be for the next read, and assert it. - for (int i = 0; i < resources.size(); i++) { - additionalFailureInfo.setInfo(getFailureLabel(resources, i)); - TestResource resource = resources.get(i); - int resourceLength = resource.getExpectedBytes().length; - DataSource dataSource = createDataSource(); - DataSpec dataSpec = - new DataSpec.Builder() - .setUri(resource.getUri()) - .setPosition(resourceLength - 1) - .setLength(2) - .build(); - try { - long length = dataSource.open(dataSpec); - byte[] data = DataSourceUtil.readExactly(dataSource, /* length= */ 1); - // TODO: Decide what the allowed behavior should be for the next read, and assert it. - - // The DataSource.open() contract requires the returned length to equal the length in the - // DataSpec if set. This is true even though the DataSource implementation may know that - // fewer bytes will be read in this case. - assertThat(length).isEqualTo(2); - byte[] expectedData = - Arrays.copyOfRange(resource.getExpectedBytes(), resourceLength - 1, resourceLength); - assertThat(data).isEqualTo(expectedData); - } finally { - dataSource.close(); - } - additionalFailureInfo.setInfo(null); - } + // The DataSource.open() contract requires the returned length to equal the length in + // the DataSpec if set. This is true even though the DataSource implementation may know + // that fewer bytes will be read in this case. + assertThat(length).isEqualTo(2); + byte[] expectedData = + Arrays.copyOfRange(resource.getExpectedBytes(), resourceLength - 1, resourceLength); + assertThat(data).isEqualTo(expectedData); + } finally { + dataSource.close(); + } + }); } /** @@ -354,293 +331,323 @@ public abstract class DataSourceContractTest { */ @Test public void unboundedDataSpecWithGzipFlag_readUntilEnd() throws Exception { - ImmutableList resources = getTestResources(); - Assertions.checkArgument(!resources.isEmpty(), "Must provide at least one test resource."); + forAllTestResourcesAndDataSources( + (resource, dataSource) -> { + try { + long length = + dataSource.open( + new DataSpec.Builder() + .setUri(resource.getUri()) + .setFlags(DataSpec.FLAG_ALLOW_GZIP) + .build()); + byte[] data = + unboundedReadsAreIndefinite() + ? DataSourceUtil.readExactly(dataSource, resource.getExpectedBytes().length) + : DataSourceUtil.readToEnd(dataSource); - for (int i = 0; i < resources.size(); i++) { - additionalFailureInfo.setInfo(getFailureLabel(resources, i)); - TestResource resource = resources.get(i); - DataSource dataSource = createDataSource(); - try { - long length = - dataSource.open( - new DataSpec.Builder() - .setUri(resource.getUri()) - .setFlags(DataSpec.FLAG_ALLOW_GZIP) - .build()); - byte[] data = - unboundedReadsAreIndefinite() - ? DataSourceUtil.readExactly(dataSource, resource.getExpectedBytes().length) - : DataSourceUtil.readToEnd(dataSource); - - if (length != C.LENGTH_UNSET) { - assertThat(length).isEqualTo(resource.getExpectedBytes().length); - } - assertThat(data).isEqualTo(resource.getExpectedBytes()); - } finally { - dataSource.close(); - } - additionalFailureInfo.setInfo(null); - } + if (length != C.LENGTH_UNSET) { + assertThat(length).isEqualTo(resource.getExpectedBytes().length); + } + assertThat(data).isEqualTo(resource.getExpectedBytes()); + } finally { + dataSource.close(); + } + }); } @Test public void uriSchemeIsCaseInsensitive() throws Exception { - ImmutableList resources = getTestResources(); - Assertions.checkArgument(!resources.isEmpty(), "Must provide at least one test resource."); + forAllTestResourcesAndDataSources( + (resource, dataSource) -> { + @Nullable String scheme = resource.getUri().getScheme(); + if (scheme == null) { + // No scheme for which to check case-insensitivity. + return; + } + Uri uri = + resource + .getUri() + .buildUpon() + .scheme(invertAsciiCaseOfEveryOtherCharacter(scheme)) + .build(); + try { + long length = dataSource.open(new DataSpec.Builder().setUri(uri).build()); + byte[] data = + unboundedReadsAreIndefinite() + ? DataSourceUtil.readExactly(dataSource, resource.getExpectedBytes().length) + : DataSourceUtil.readToEnd(dataSource); - for (int i = 0; i < resources.size(); i++) { - additionalFailureInfo.setInfo(getFailureLabel(resources, i)); - TestResource resource = resources.get(i); - @Nullable String scheme = resource.getUri().getScheme(); - if (scheme == null) { - // No scheme for which to check case-insensitivity. - continue; - } - DataSource dataSource = createDataSource(); - Uri uri = - resource - .getUri() - .buildUpon() - .scheme(invertAsciiCaseOfEveryOtherCharacter(scheme)) - .build(); - try { - long length = dataSource.open(new DataSpec.Builder().setUri(uri).build()); - byte[] data = - unboundedReadsAreIndefinite() - ? DataSourceUtil.readExactly(dataSource, resource.getExpectedBytes().length) - : DataSourceUtil.readToEnd(dataSource); - - if (length != C.LENGTH_UNSET) { - assertThat(length).isEqualTo(resource.getExpectedBytes().length); - } - assertThat(data).isEqualTo(resource.getExpectedBytes()); - } finally { - dataSource.close(); - } - additionalFailureInfo.setInfo(null); - } + if (length != C.LENGTH_UNSET) { + assertThat(length).isEqualTo(resource.getExpectedBytes().length); + } + assertThat(data).isEqualTo(resource.getExpectedBytes()); + } finally { + dataSource.close(); + } + }); } @Test public void resourceNotFound() throws Exception { - DataSource dataSource = createDataSource(); - assertThrows(IOException.class, () -> dataSource.open(new DataSpec(getNotFoundUri()))); - dataSource.close(); + forAllDataSources( + dataSource -> { + assertThrows(IOException.class, () -> dataSource.open(new DataSpec(getNotFoundUri()))); + dataSource.close(); + }); } @Test public void transferListenerCallbacks() throws Exception { - ImmutableList resources = getTestResources(); - Assertions.checkArgument(!resources.isEmpty(), "Must provide at least one test resource."); + forAllTestResourcesAndDataSources( + (resource, dataSource) -> { + FakeTransferListener listener = spy(new FakeTransferListener()); + dataSource.addTransferListener(listener); + InOrder inOrder = Mockito.inOrder(listener); + @Nullable DataSource callbackSource = getTransferListenerDataSource(); + if (callbackSource == null) { + callbackSource = dataSource; + } + DataSpec reportedDataSpec = null; + boolean reportedNetwork = false; - for (int i = 0; i < resources.size(); i++) { - additionalFailureInfo.setInfo(getFailureLabel(resources, i)); - DataSource dataSource = createDataSource(); - FakeTransferListener listener = spy(new FakeTransferListener()); - dataSource.addTransferListener(listener); - InOrder inOrder = Mockito.inOrder(listener); - @Nullable DataSource callbackSource = getTransferListenerDataSource(); - if (callbackSource == null) { - callbackSource = dataSource; - } - DataSpec reportedDataSpec = null; - boolean reportedNetwork = false; + DataSpec dataSpec = new DataSpec.Builder().setUri(resource.getUri()).build(); + try { + dataSource.open(dataSpec); - TestResource resource = resources.get(i); - DataSpec dataSpec = new DataSpec.Builder().setUri(resource.getUri()).build(); - try { - dataSource.open(dataSpec); + // Verify onTransferInitializing() and onTransferStart() have been called exactly from + // DataSource.open(). + ArgumentCaptor dataSpecArgumentCaptor = + ArgumentCaptor.forClass(DataSpec.class); + ArgumentCaptor isNetworkArgumentCaptor = + ArgumentCaptor.forClass(Boolean.class); + inOrder + .verify(listener) + .onTransferInitializing( + eq(callbackSource), + dataSpecArgumentCaptor.capture(), + isNetworkArgumentCaptor.capture()); + reportedDataSpec = dataSpecArgumentCaptor.getValue(); + reportedNetwork = isNetworkArgumentCaptor.getValue(); + inOrder + .verify(listener) + .onTransferStart(callbackSource, castNonNull(reportedDataSpec), reportedNetwork); + inOrder.verifyNoMoreInteractions(); - // Verify onTransferInitializing() and onTransferStart() have been called exactly from - // DataSource.open(). - ArgumentCaptor dataSpecArgumentCaptor = ArgumentCaptor.forClass(DataSpec.class); - ArgumentCaptor isNetworkArgumentCaptor = ArgumentCaptor.forClass(Boolean.class); - inOrder - .verify(listener) - .onTransferInitializing( - eq(callbackSource), - dataSpecArgumentCaptor.capture(), - isNetworkArgumentCaptor.capture()); - reportedDataSpec = dataSpecArgumentCaptor.getValue(); - reportedNetwork = isNetworkArgumentCaptor.getValue(); - inOrder - .verify(listener) - .onTransferStart(callbackSource, castNonNull(reportedDataSpec), reportedNetwork); - inOrder.verifyNoMoreInteractions(); + if (unboundedReadsAreIndefinite()) { + DataSourceUtil.readExactly(dataSource, resource.getExpectedBytes().length); + } else { + DataSourceUtil.readToEnd(dataSource); + } + // Verify sufficient onBytesTransferred() callbacks have been triggered before closing + // the DataSource. + assertThat(listener.bytesTransferred).isAtLeast(resource.getExpectedBytes().length); - if (unboundedReadsAreIndefinite()) { - DataSourceUtil.readExactly(dataSource, resource.getExpectedBytes().length); - } else { - DataSourceUtil.readToEnd(dataSource); - } - // Verify sufficient onBytesTransferred() callbacks have been triggered before closing the - // DataSource. - assertThat(listener.bytesTransferred).isAtLeast(resource.getExpectedBytes().length); - - } finally { - dataSource.close(); - inOrder - .verify(listener) - .onTransferEnd(callbackSource, castNonNull(reportedDataSpec), reportedNetwork); - inOrder.verifyNoMoreInteractions(); - } - additionalFailureInfo.setInfo(null); - } + } finally { + dataSource.close(); + inOrder + .verify(listener) + .onTransferEnd(callbackSource, castNonNull(reportedDataSpec), reportedNetwork); + inOrder.verifyNoMoreInteractions(); + } + }); } @Test public void resourceNotFound_transferListenerCallbacks() throws Exception { - DataSource dataSource = createDataSource(); - TransferListener listener = mock(TransferListener.class); - dataSource.addTransferListener(listener); - @Nullable DataSource callbackSource = getTransferListenerDataSource(); - if (callbackSource == null) { - callbackSource = dataSource; - } + forAllDataSources( + dataSource -> { + TransferListener listener = mock(TransferListener.class); + dataSource.addTransferListener(listener); + @Nullable DataSource callbackSource = getTransferListenerDataSource(); + if (callbackSource == null) { + callbackSource = dataSource; + } - assertThrows(IOException.class, () -> dataSource.open(new DataSpec(getNotFoundUri()))); + assertThrows(IOException.class, () -> dataSource.open(new DataSpec(getNotFoundUri()))); - // Verify onTransferInitializing() has been called exactly from DataSource.open(). - verify(listener).onTransferInitializing(eq(callbackSource), any(), anyBoolean()); - verifyNoMoreInteractions(listener); + // Verify onTransferInitializing() has been called exactly from DataSource.open(). + verify(listener).onTransferInitializing(eq(callbackSource), any(), anyBoolean()); + verifyNoMoreInteractions(listener); - dataSource.close(); - verifyNoMoreInteractions(listener); + dataSource.close(); + verifyNoMoreInteractions(listener); + }); } @Test public void getUri_returnsNonNullValueOnlyWhileOpen() throws Exception { - ImmutableList resources = getTestResources(); - Assertions.checkArgument(!resources.isEmpty(), "Must provide at least one test resource."); + forAllTestResourcesAndDataSources( + (resource, dataSource) -> { + try { + assertThat(dataSource.getUri()).isNull(); - for (int i = 0; i < resources.size(); i++) { - additionalFailureInfo.setInfo(getFailureLabel(resources, i)); - TestResource resource = resources.get(i); - DataSource dataSource = createDataSource(); - try { - assertThat(dataSource.getUri()).isNull(); + dataSource.open(new DataSpec(resource.getUri())); - dataSource.open(new DataSpec(resource.getUri())); - - assertThat(dataSource.getUri()).isNotNull(); - } finally { - dataSource.close(); - } - assertThat(dataSource.getUri()).isNull(); - - additionalFailureInfo.setInfo(null); - } + assertThat(dataSource.getUri()).isNotNull(); + } finally { + dataSource.close(); + } + assertThat(dataSource.getUri()).isNull(); + }); } @Test public void getUri_resourceNotFound_returnsNullIfNotOpened() throws Exception { - DataSource dataSource = createDataSource(); + forAllDataSources( + dataSource -> { + assertThat(dataSource.getUri()).isNull(); - assertThat(dataSource.getUri()).isNull(); + assertThrows(IOException.class, () -> dataSource.open(new DataSpec(getNotFoundUri()))); + dataSource.close(); - assertThrows(IOException.class, () -> dataSource.open(new DataSpec(getNotFoundUri()))); - dataSource.close(); - - assertThat(dataSource.getUri()).isNull(); + assertThat(dataSource.getUri()).isNull(); + }); } @Test public void getResponseHeaders_noNullKeysOrValues() throws Exception { - ImmutableList resources = getTestResources(); - Assertions.checkArgument(!resources.isEmpty(), "Must provide at least one test resource."); + forAllTestResourcesAndDataSources( + (resource, dataSource) -> { + try { + dataSource.open(new DataSpec(resource.getUri())); - for (int i = 0; i < resources.size(); i++) { - additionalFailureInfo.setInfo(getFailureLabel(resources, i)); - TestResource resource = resources.get(i); - DataSource dataSource = createDataSource(); - try { - dataSource.open(new DataSpec(resource.getUri())); - - Map> responseHeaders = dataSource.getResponseHeaders(); - assertThat(responseHeaders).doesNotContainKey(null); - assertThat(responseHeaders.values()).doesNotContain(null); - for (List value : responseHeaders.values()) { - assertThat(value).doesNotContain(null); - } - } finally { - dataSource.close(); - } - additionalFailureInfo.setInfo(null); - } + Map> responseHeaders = dataSource.getResponseHeaders(); + assertThat(responseHeaders).doesNotContainKey(null); + assertThat(responseHeaders.values()).doesNotContain(null); + for (List value : responseHeaders.values()) { + assertThat(value).doesNotContain(null); + } + } finally { + dataSource.close(); + } + }); } @Test public void getResponseHeaders_caseInsensitive() throws Exception { - ImmutableList resources = getTestResources(); - Assertions.checkArgument(!resources.isEmpty(), "Must provide at least one test resource."); + forAllTestResourcesAndDataSources( + (resource, dataSource) -> { + try { + dataSource.open(new DataSpec(resource.getUri())); - for (int i = 0; i < resources.size(); i++) { - additionalFailureInfo.setInfo(getFailureLabel(resources, i)); - TestResource resource = resources.get(i); - DataSource dataSource = createDataSource(); - try { - dataSource.open(new DataSpec(resource.getUri())); - - Map> responseHeaders = dataSource.getResponseHeaders(); - for (String key : responseHeaders.keySet()) { - String caseFlippedKey = invertAsciiCaseOfEveryOtherCharacter(key); - assertWithMessage("key='%s', caseFlippedKey='%s'", key, caseFlippedKey) - .that(responseHeaders.get(caseFlippedKey)) - .isEqualTo(responseHeaders.get(key)); - } - } finally { - dataSource.close(); - } - additionalFailureInfo.setInfo(null); - } + Map> responseHeaders = dataSource.getResponseHeaders(); + for (String key : responseHeaders.keySet()) { + String caseFlippedKey = invertAsciiCaseOfEveryOtherCharacter(key); + assertWithMessage("key='%s', caseFlippedKey='%s'", key, caseFlippedKey) + .that(responseHeaders.get(caseFlippedKey)) + .isEqualTo(responseHeaders.get(key)); + } + } finally { + dataSource.close(); + } + }); } @Test public void getResponseHeaders_isEmptyWhileNotOpen() throws Exception { - ImmutableList resources = getTestResources(); - Assertions.checkArgument(!resources.isEmpty(), "Must provide at least one test resource."); + forAllTestResourcesAndDataSources( + (resource, dataSource) -> { + try { + assertThat(dataSource.getResponseHeaders()).isEmpty(); - for (int i = 0; i < resources.size(); i++) { - additionalFailureInfo.setInfo(getFailureLabel(resources, i)); - TestResource resource = resources.get(i); - DataSource dataSource = createDataSource(); - try { - assertThat(dataSource.getResponseHeaders()).isEmpty(); - - dataSource.open(new DataSpec(resource.getUri())); - } finally { - dataSource.close(); - } - assertThat(dataSource.getResponseHeaders()).isEmpty(); - - additionalFailureInfo.setInfo(null); - } + dataSource.open(new DataSpec(resource.getUri())); + } finally { + dataSource.close(); + } + assertThat(dataSource.getResponseHeaders()).isEmpty(); + }); } @Test public void getResponseHeaders_resourceNotFound_isEmptyWhileNotOpen() throws Exception { - DataSource dataSource = createDataSource(); + forAllDataSources( + dataSource -> { + assertThat(dataSource.getResponseHeaders()).isEmpty(); - assertThat(dataSource.getResponseHeaders()).isEmpty(); + assertThrows(IOException.class, () -> dataSource.open(new DataSpec(getNotFoundUri()))); + dataSource.close(); - assertThrows(IOException.class, () -> dataSource.open(new DataSpec(getNotFoundUri()))); - dataSource.close(); - - assertThat(dataSource.getResponseHeaders()).isEmpty(); + assertThat(dataSource.getResponseHeaders()).isEmpty(); + }); } - /** Build a label to make it clear which resource caused a given test failure. */ - private static String getFailureLabel(List resources, int i) { + private interface TestResourceAndDataSourceTest { + void run(TestResource resource, DataSource dataSource) throws Exception; + } + + private interface DataSourceTest { + void run(DataSource dataSource) throws Exception; + } + + private void forAllTestResourcesAndDataSources(TestResourceAndDataSourceTest test) + throws Exception { + ImmutableList resources = getTestResources(); + Assertions.checkArgument(!resources.isEmpty(), "Must provide at least one test resource."); + for (int i = 0; i < resources.size(); i++) { + List dataSources = createDataSourcesInternal(); + for (int j = 0; j < dataSources.size(); j++) { + additionalFailureInfo.setInfo(getFailureLabel(resources, i, dataSources, j)); + test.run(resources.get(i), dataSources.get(j)); + additionalFailureInfo.setInfo(null); + } + } + } + + private void forAllDataSources(DataSourceTest test) throws Exception { + List dataSources = createDataSourcesInternal(); + for (int i = 0; i < dataSources.size(); i++) { + additionalFailureInfo.setInfo(getDataSourceLabel(dataSources, i)); + test.run(dataSources.get(i)); + additionalFailureInfo.setInfo(null); + } + } + + private List createDataSourcesInternal() throws Exception { + try { + List dataSources = createDataSources(); + checkState(!dataSources.isEmpty(), "Must provide at least on DataSource"); + assertThrows(UnsupportedOperationException.class, this::createDataSource); + return dataSources; + } catch (UnsupportedOperationException e) { + // Expected if createDataSources is not implemented. + return ImmutableList.of(createDataSource()); + } + } + + /** Build a label to make it clear which resource and data source caused a given test failure. */ + private static String getFailureLabel( + List resources, + int resourceIndex, + List dataSources, + int dataSourceIndex) { + String resourceLabel = getResourceLabel(resources, resourceIndex); + String dataSourceLabel = getDataSourceLabel(dataSources, dataSourceIndex); + if (resourceLabel.isEmpty()) { + return dataSourceLabel; + } else if (dataSourceLabel.isEmpty()) { + return resourceLabel; + } else { + return dataSourceLabel + ", " + resourceLabel; + } + } + + private static String getResourceLabel(List resources, int resourceIndex) { if (resources.size() == 1) { return ""; - } else if (resources.get(i).getName() != null) { - return "resource name: " + resources.get(i).getName(); + } else if (resources.get(resourceIndex).getName() != null) { + return "resource name: " + resources.get(resourceIndex).getName(); } else { - return String.format("resource[%s]", i); + return String.format("resource[%s]", resourceIndex); } } + private static String getDataSourceLabel(List dataSources, int dataSourceIndex) { + if (dataSources.size() == 1) { + return ""; + } + return String.format("dataSource[%s]", dataSourceIndex); + } + private static String invertAsciiCaseOfEveryOtherCharacter(String input) { StringBuilder result = new StringBuilder(); for (int i = 0; i < input.length(); i++) {