From 219565c15e9aaee90266b9d07d94286e0423d49c Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 22 Oct 2024 08:17:03 -0700 Subject: [PATCH] `DataSourceContractTest`: Add expected response headers PiperOrigin-RevId: 688556440 --- .../test/utils/DataSourceContractTest.java | 91 ++++++++++++++++++- .../test/utils/HttpDataSourceTestEnv.java | 32 ++++++- .../test/utils/WebServerDispatcher.java | 33 ++++++- 3 files changed, 148 insertions(+), 8 deletions(-) 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 7a5175cbab..00e18ad8f0 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 @@ -43,12 +43,15 @@ import androidx.media3.datasource.DataSpec; import androidx.media3.datasource.TransferListener; import com.google.common.base.Ascii; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.ForOverride; import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Set; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.junit.Ignore; import org.junit.Rule; @@ -569,6 +572,31 @@ public abstract class DataSourceContractTest { }); } + @Test + public void getResponseHeaders_returnsExpectedValues() throws Exception { + forAllTestResourcesAndDataSources( + (resource, dataSource) -> { + try { + dataSource.open(new DataSpec(resource.getUri())); + // Iterate over the expected headers (instead of using Truth's batch + // containsAtLeastEntriesIn() method) in order to leverage the case-insensitivity of + // the DataSource.getResponseHeaders() implementation. + Map> actualHeaders = dataSource.getResponseHeaders(); + for (Map.Entry> expectedHeaders : + resource.getResponseHeaders().entrySet()) { + assertWithMessage("Header values for key=%s", expectedHeaders.getKey()) + .that(actualHeaders.get(expectedHeaders.getKey())) + .isEqualTo(expectedHeaders.getValue()); + } + for (String unexpectedKey : resource.getUnexpectedResponseHeaderKeys()) { + assertThat(actualHeaders).doesNotContainKey(unexpectedKey); + } + } finally { + dataSource.close(); + } + }); + } + @Test public void getResponseHeaders_noNullKeysOrValues() throws Exception { forAllTestResourcesAndDataSources( @@ -741,12 +769,22 @@ public abstract class DataSourceContractTest { @Nullable private final String name; private final Uri uri; private final Uri resolvedUri; + private final Map> responseHeaders; + private final Set unexpectedResponseHeaderKeys; private final byte[] expectedBytes; - private TestResource(@Nullable String name, Uri uri, Uri resolvedUri, byte[] expectedBytes) { + private TestResource( + @Nullable String name, + Uri uri, + Uri resolvedUri, + Map> responseHeaders, + Set unexpectedResponseHeaderKeys, + byte[] expectedBytes) { this.name = name; this.uri = uri; this.resolvedUri = resolvedUri; + this.responseHeaders = responseHeaders; + this.unexpectedResponseHeaderKeys = unexpectedResponseHeaderKeys; this.expectedBytes = expectedBytes; } @@ -769,6 +807,25 @@ public abstract class DataSourceContractTest { return resolvedUri; } + /** + * Returns the headers associated with this resource that are expected to be present in {@link + * DataSource#getResponseHeaders()}. + * + *

This doesn't have to be an exhaustive list, extra headers in {@link + * DataSource#getResponseHeaders()} are ignored. + */ + public Map> getResponseHeaders() { + return responseHeaders; + } + + /** + * Returns the keys that must not be present in {@link DataSource#getResponseHeaders()} + * when reading this resource. + */ + public Set getUnexpectedResponseHeaderKeys() { + return unexpectedResponseHeaderKeys; + } + /** Returns the expected contents of this resource. */ public byte[] getExpectedBytes() { return expectedBytes; @@ -779,8 +836,15 @@ public abstract class DataSourceContractTest { private @MonotonicNonNull String name; private @MonotonicNonNull Uri uri; private @MonotonicNonNull Uri resolvedUri; + private Map> responseHeaders; + private Set unexpectedResponseHeaderKeys; private byte @MonotonicNonNull [] expectedBytes; + public Builder() { + responseHeaders = ImmutableMap.of(); + unexpectedResponseHeaderKeys = ImmutableSet.of(); + } + /** * Sets a human-readable name for this resource which will be shown in test failure messages. */ @@ -822,6 +886,29 @@ public abstract class DataSourceContractTest { return this; } + /** + * Sets the headers associated with this resource that are expected to be present in {@link + * DataSource#getResponseHeaders()}. + * + *

This doesn't have to be an exhaustive list, extra headers in {@link + * DataSource#getResponseHeaders()} are ignored. Defaults to an empty map. + */ + @CanIgnoreReturnValue + public Builder setResponseHeaders(Map> responseHeaders) { + this.responseHeaders = responseHeaders; + return this; + } + + /** + * Sets the keys that must not be present in {@link DataSource#getResponseHeaders()} + * when reading this resource. Defaults to an empty set. + */ + @CanIgnoreReturnValue + public Builder setUnexpectedResponseHeaderKeys(Set unexpectedResponseHeaderKeys) { + this.unexpectedResponseHeaderKeys = unexpectedResponseHeaderKeys; + return this; + } + /** * Sets the expected contents of this resource. * @@ -839,6 +926,8 @@ public abstract class DataSourceContractTest { name, checkNotNull(uri), resolvedUri != null ? resolvedUri : uri, + ImmutableMap.copyOf(responseHeaders), + ImmutableSet.copyOf(unexpectedResponseHeaderKeys), checkNotNull(expectedBytes)); } } diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/HttpDataSourceTestEnv.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/HttpDataSourceTestEnv.java index 68d5e8b864..b5fd8804db 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/HttpDataSourceTestEnv.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/HttpDataSourceTestEnv.java @@ -22,7 +22,12 @@ import android.net.Uri; import androidx.media3.common.util.UnstableApi; import androidx.media3.datasource.HttpDataSource; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import com.google.common.net.HttpHeaders; import java.io.IOException; +import java.util.List; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -33,12 +38,19 @@ import org.junit.rules.ExternalResource; /** A JUnit {@link Rule} that creates test resources for {@link HttpDataSource} contract tests. */ @UnstableApi public class HttpDataSourceTestEnv extends ExternalResource { + + private static final ImmutableListMultimap EXTRA_HEADERS = + ImmutableListMultimap.builder() + .putAll("X-Test-Header", "test value1", "test value2") + .build(); + private static int seed = 0; private static final WebServerDispatcher.Resource RANGE_SUPPORTED = new WebServerDispatcher.Resource.Builder() .setPath("/supports/range-requests") .setData(TestUtil.buildTestData(/* length= */ 20, seed++)) .supportsRangeRequests(true) + .setExtraResponseHeaders(EXTRA_HEADERS) .build(); private static final WebServerDispatcher.Resource RANGE_SUPPORTED_LENGTH_UNKNOWN = @@ -47,6 +59,7 @@ public class HttpDataSourceTestEnv extends ExternalResource { .setData(TestUtil.buildTestData(/* length= */ 20, seed++)) .supportsRangeRequests(true) .resolvesToUnknownLength(true) + .setExtraResponseHeaders(EXTRA_HEADERS) .build(); private static final WebServerDispatcher.Resource RANGE_NOT_SUPPORTED = @@ -54,6 +67,7 @@ public class HttpDataSourceTestEnv extends ExternalResource { .setPath("/doesnt/support/range-requests") .setData(TestUtil.buildTestData(/* length= */ 20, seed++)) .supportsRangeRequests(false) + .setExtraResponseHeaders(EXTRA_HEADERS) .build(); private static final WebServerDispatcher.Resource RANGE_NOT_SUPPORTED_LENGTH_UNKNOWN = @@ -62,6 +76,7 @@ public class HttpDataSourceTestEnv extends ExternalResource { .setData(TestUtil.buildTestData(/* length= */ 20, seed++)) .supportsRangeRequests(false) .resolvesToUnknownLength(true) + .setExtraResponseHeaders(EXTRA_HEADERS) .build(); private static final WebServerDispatcher.Resource GZIP_ENABLED = @@ -69,6 +84,7 @@ public class HttpDataSourceTestEnv extends ExternalResource { .setPath("/gzip/enabled") .setData(TestUtil.buildTestData(/* length= */ 20, seed++)) .setGzipSupport(WebServerDispatcher.Resource.GZIP_SUPPORT_ENABLED) + .setExtraResponseHeaders(EXTRA_HEADERS) .build(); private static final WebServerDispatcher.Resource GZIP_FORCED = @@ -76,6 +92,7 @@ public class HttpDataSourceTestEnv extends ExternalResource { .setPath("/gzip/forced") .setData(TestUtil.buildTestData(/* length= */ 20, seed++)) .setGzipSupport(WebServerDispatcher.Resource.GZIP_SUPPORT_FORCED) + .setExtraResponseHeaders(EXTRA_HEADERS) .build(); private static final WebServerDispatcher.Resource REDIRECTS_TO_RANGE_SUPPORTED = @@ -147,10 +164,15 @@ public class HttpDataSourceTestEnv extends ExternalResource { private DataSourceContractTest.TestResource createTestResource( String name, WebServerDispatcher.Resource resource) { - return new DataSourceContractTest.TestResource.Builder() - .setName(name) - .setUri(Uri.parse(originServer.url(resource.getPath()).toString())) - .setExpectedBytes(resource.getData()) - .build(); + DataSourceContractTest.TestResource.Builder testResource = + new DataSourceContractTest.TestResource.Builder() + .setName(name) + .setUri(Uri.parse(originServer.url(resource.getPath()).toString())) + .setResponseHeaders(Maps.transformValues(EXTRA_HEADERS.asMap(), v -> (List) v)) + .setExpectedBytes(resource.getData()); + if (resource.resolvesToUnknownLength()) { + testResource.setUnexpectedResponseHeaderKeys(ImmutableSet.of(HttpHeaders.CONTENT_LENGTH)); + } + return testResource.build(); } } diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/WebServerDispatcher.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/WebServerDispatcher.java index 2dc0af52aa..562ba62c12 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/WebServerDispatcher.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/WebServerDispatcher.java @@ -32,8 +32,10 @@ import androidx.media3.common.util.Util; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -41,6 +43,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import okhttp3.mockwebserver.Dispatcher; @@ -102,10 +105,12 @@ public class WebServerDispatcher extends Dispatcher { private boolean supportsRangeRequests; private boolean resolvesToUnknownLength; private @GzipSupport int gzipSupport; + private ImmutableListMultimap extraResponseHeaders; /** Constructs an instance. */ public Builder() { this.gzipSupport = GZIP_SUPPORT_DISABLED; + this.extraResponseHeaders = ImmutableListMultimap.of(); } private Builder(Resource resource) { @@ -114,6 +119,7 @@ public class WebServerDispatcher extends Dispatcher { this.supportsRangeRequests = resource.supportsRangeRequests(); this.resolvesToUnknownLength = resource.resolvesToUnknownLength(); this.gzipSupport = resource.getGzipSupport(); + this.extraResponseHeaders = resource.getExtraResponseHeaders(); } /** @@ -175,6 +181,17 @@ public class WebServerDispatcher extends Dispatcher { return this; } + /** + * Sets the extra response headers that should be attached. + * + * @return this builder, for convenience. + */ + @CanIgnoreReturnValue + public Builder setExtraResponseHeaders(Multimap extraResponseHeaders) { + this.extraResponseHeaders = ImmutableListMultimap.copyOf(extraResponseHeaders); + return this; + } + /** Builds the {@link Resource}. */ public Resource build() { if (gzipSupport != GZIP_SUPPORT_DISABLED) { @@ -186,7 +203,8 @@ public class WebServerDispatcher extends Dispatcher { checkNotNull(data), supportsRangeRequests, resolvesToUnknownLength, - gzipSupport); + gzipSupport, + extraResponseHeaders); } } @@ -195,18 +213,21 @@ public class WebServerDispatcher extends Dispatcher { private final boolean supportsRangeRequests; private final boolean resolvesToUnknownLength; private final @GzipSupport int gzipSupport; + ImmutableListMultimap extraResponseHeaders; private Resource( String path, byte[] data, boolean supportsRangeRequests, boolean resolvesToUnknownLength, - @GzipSupport int gzipSupport) { + @GzipSupport int gzipSupport, + ImmutableListMultimap extraResponseHeaders) { this.path = path; this.data = data; this.supportsRangeRequests = supportsRangeRequests; this.resolvesToUnknownLength = resolvesToUnknownLength; this.gzipSupport = gzipSupport; + this.extraResponseHeaders = extraResponseHeaders; } /** Returns the path this resource is available at. */ @@ -234,6 +255,11 @@ public class WebServerDispatcher extends Dispatcher { return gzipSupport; } + /** Returns the extra response headers that should be attached. */ + public ImmutableListMultimap getExtraResponseHeaders() { + return extraResponseHeaders; + } + /** Returns a new {@link Builder} initialized with the values from this instance. */ public Builder buildUpon() { return new Builder(this); @@ -270,6 +296,9 @@ public class WebServerDispatcher extends Dispatcher { return response.setResponseCode(404); } Resource resource = checkNotNull(resourcesByPath.get(requestPath)); + for (Map.Entry extraHeader : resource.getExtraResponseHeaders().entries()) { + response.addHeader(extraHeader.getKey(), extraHeader.getValue()); + } byte[] resourceData = resource.getData(); if (resource.supportsRangeRequests()) { response.setHeader("Accept-Ranges", "bytes");