Commit 3069251b by ibaker Committed by Oliver Woodman

Add gzip support to WebServerDispatcher

Add a test to DataSourceContractTest that asserts the gzip flag is
either ignored or handled correctly.

Add a test resource to DefaultHttpDataSourceContracTest that enables
gzip compression on the 'server' and checks it's handled correctly by
the client.

PiperOrigin-RevId: 352574359
parent dd1b1c08
...@@ -36,6 +36,7 @@ dependencies { ...@@ -36,6 +36,7 @@ dependencies {
testImplementation 'junit:junit:' + junitVersion testImplementation 'junit:junit:' + junitVersion
testImplementation 'com.google.truth:truth:' + truthVersion testImplementation 'com.google.truth:truth:' + truthVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation project(modulePrefix + 'testutils')
} }
ext { ext {
......
...@@ -91,6 +91,7 @@ import java.util.concurrent.Executors; ...@@ -91,6 +91,7 @@ import java.util.concurrent.Executors;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.zip.DataFormatException; import java.util.zip.DataFormatException;
import java.util.zip.GZIPOutputStream;
import java.util.zip.Inflater; import java.util.zip.Inflater;
import org.checkerframework.checker.initialization.qual.UnknownInitialization; import org.checkerframework.checker.initialization.qual.UnknownInitialization;
import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.compatqual.NullableType;
...@@ -2121,6 +2122,17 @@ public final class Util { ...@@ -2121,6 +2122,17 @@ public final class Util {
return initialValue; return initialValue;
} }
/** Compresses {@code input} using gzip and returns the result in a newly allocated byte array. */
public static byte[] gzip(byte[] input) {
ByteArrayOutputStream output = new ByteArrayOutputStream();
try (GZIPOutputStream os = new GZIPOutputStream(output)) {
os.write(input);
} catch (IOException e) {
throw new AssertionError(e);
}
return output.toByteArray();
}
/** /**
* Absolute <i>get</i> method for reading an int value in {@link ByteOrder#BIG_ENDIAN} in a {@link * Absolute <i>get</i> method for reading an int value in {@link ByteOrder#BIG_ENDIAN} in a {@link
* ByteBuffer}. Same as {@link ByteBuffer#getInt(int)} except the buffer's order as returned by * ByteBuffer}. Same as {@link ByteBuffer#getInt(int)} except the buffer's order as returned by
......
...@@ -20,6 +20,7 @@ import static com.google.android.exoplayer2.util.Util.binarySearchFloor; ...@@ -20,6 +20,7 @@ import static com.google.android.exoplayer2.util.Util.binarySearchFloor;
import static com.google.android.exoplayer2.util.Util.escapeFileName; import static com.google.android.exoplayer2.util.Util.escapeFileName;
import static com.google.android.exoplayer2.util.Util.getCodecsOfType; import static com.google.android.exoplayer2.util.Util.getCodecsOfType;
import static com.google.android.exoplayer2.util.Util.getStringForTime; import static com.google.android.exoplayer2.util.Util.getStringForTime;
import static com.google.android.exoplayer2.util.Util.gzip;
import static com.google.android.exoplayer2.util.Util.minValue; import static com.google.android.exoplayer2.util.Util.minValue;
import static com.google.android.exoplayer2.util.Util.parseXsDateTime; import static com.google.android.exoplayer2.util.Util.parseXsDateTime;
import static com.google.android.exoplayer2.util.Util.parseXsDuration; import static com.google.android.exoplayer2.util.Util.parseXsDuration;
...@@ -37,6 +38,9 @@ import android.text.style.UnderlineSpan; ...@@ -37,6 +38,9 @@ import android.text.style.UnderlineSpan;
import android.util.SparseLongArray; import android.util.SparseLongArray;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.common.io.ByteStreams;
import java.io.ByteArrayInputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.ArrayList; import java.util.ArrayList;
...@@ -45,6 +49,7 @@ import java.util.Formatter; ...@@ -45,6 +49,7 @@ import java.util.Formatter;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.Random; import java.util.Random;
import java.util.zip.Deflater; import java.util.zip.Deflater;
import java.util.zip.GZIPInputStream;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
...@@ -928,6 +933,17 @@ public class UtilTest { ...@@ -928,6 +933,17 @@ public class UtilTest {
} }
@Test @Test
public void gzip_resultInflatesBackToOriginalValue() throws Exception {
byte[] input = TestUtil.buildTestData(20);
byte[] deflated = gzip(input);
byte[] inflated =
ByteStreams.toByteArray(new GZIPInputStream(new ByteArrayInputStream(deflated)));
assertThat(inflated).isEqualTo(input);
}
@Test
public void getBigEndianInt_fromBigEndian() { public void getBigEndianInt_fromBigEndian() {
byte[] bytes = {0x1F, 0x2E, 0x3D, 0x4C}; byte[] bytes = {0x1F, 0x2E, 0x3D, 0x4C};
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN); ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
......
...@@ -194,6 +194,43 @@ public abstract class DataSourceContractTest { ...@@ -194,6 +194,43 @@ public abstract class DataSourceContractTest {
} }
} }
/**
* {@link DataSpec#FLAG_ALLOW_GZIP} should either be ignored by {@link DataSource}
* implementations, or correctly handled (i.e. the data is decompressed before being returned from
* {@link DataSource#read(byte[], int, int)}).
*/
@Test
public void gzipFlagDoesntAffectReturnedData() throws Exception {
ImmutableList<TestResource> 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);
DataSource dataSource = createDataSource();
try {
long length =
dataSource.open(
new DataSpec.Builder()
.setUri(resource.getUri())
.setFlags(DataSpec.FLAG_ALLOW_GZIP)
.build());
byte[] data =
resource.isEndOfInputExpected()
? Util.readToEnd(dataSource)
: Util.readExactly(dataSource, resource.getExpectedBytes().length);
if (length != C.LENGTH_UNSET) {
assertThat(length).isEqualTo(resource.getExpectedBytes().length);
}
assertThat(data).isEqualTo(resource.getExpectedBytes());
} finally {
dataSource.close();
}
additionalFailureInfo.setInfo(null);
}
}
@Test @Test
public void resourceNotFound() throws Exception { public void resourceNotFound() throws Exception {
DataSource dataSource = createDataSource(); DataSource dataSource = createDataSource();
......
...@@ -60,6 +60,20 @@ public class HttpDataSourceTestEnv extends ExternalResource { ...@@ -60,6 +60,20 @@ public class HttpDataSourceTestEnv extends ExternalResource {
.resolvesToUnknownLength(true) .resolvesToUnknownLength(true)
.build(); .build();
private static final WebServerDispatcher.Resource GZIP_ENABLED =
new WebServerDispatcher.Resource.Builder()
.setPath("/gzip/enabled")
.setData(TestUtil.buildTestData(/* length= */ 20, seed++))
.setGzipSupport(WebServerDispatcher.Resource.GZIP_SUPPORT_ENABLED)
.build();
private static final WebServerDispatcher.Resource GZIP_FORCED =
new WebServerDispatcher.Resource.Builder()
.setPath("/gzip/forced")
.setData(TestUtil.buildTestData(/* length= */ 20, seed++))
.setGzipSupport(WebServerDispatcher.Resource.GZIP_SUPPORT_FORCED)
.build();
private static final WebServerDispatcher.Resource REDIRECTS_TO_RANGE_SUPPORTED = private static final WebServerDispatcher.Resource REDIRECTS_TO_RANGE_SUPPORTED =
RANGE_SUPPORTED.buildUpon().setPath("/redirects/to/range/supported").build(); RANGE_SUPPORTED.buildUpon().setPath("/redirects/to/range/supported").build();
...@@ -74,6 +88,8 @@ public class HttpDataSourceTestEnv extends ExternalResource { ...@@ -74,6 +88,8 @@ public class HttpDataSourceTestEnv extends ExternalResource {
createTestResource("range not supported", RANGE_NOT_SUPPORTED), createTestResource("range not supported", RANGE_NOT_SUPPORTED),
createTestResource( createTestResource(
"range not supported, length unknown", RANGE_NOT_SUPPORTED_LENGTH_UNKNOWN), "range not supported, length unknown", RANGE_NOT_SUPPORTED_LENGTH_UNKNOWN),
createTestResource("gzip enabled", GZIP_ENABLED),
createTestResource("gzip forced", GZIP_FORCED),
createTestResource( createTestResource(
"302 redirect", REDIRECTS_TO_RANGE_SUPPORTED, /* server= */ redirectionServer)); "302 redirect", REDIRECTS_TO_RANGE_SUPPORTED, /* server= */ redirectionServer));
} }
...@@ -91,7 +107,9 @@ public class HttpDataSourceTestEnv extends ExternalResource { ...@@ -91,7 +107,9 @@ public class HttpDataSourceTestEnv extends ExternalResource {
RANGE_SUPPORTED, RANGE_SUPPORTED,
RANGE_SUPPORTED_LENGTH_UNKNOWN, RANGE_SUPPORTED_LENGTH_UNKNOWN,
RANGE_NOT_SUPPORTED, RANGE_NOT_SUPPORTED,
RANGE_NOT_SUPPORTED_LENGTH_UNKNOWN))); RANGE_NOT_SUPPORTED_LENGTH_UNKNOWN,
GZIP_ENABLED,
GZIP_FORCED)));
redirectionServer.start(); redirectionServer.start();
redirectionServer.setDispatcher( redirectionServer.setDispatcher(
......
...@@ -15,14 +15,25 @@ ...@@ -15,14 +15,25 @@
*/ */
package com.google.android.exoplayer2.testutil; package com.google.android.exoplayer2.testutil;
import static com.google.android.exoplayer2.testutil.WebServerDispatcher.Resource.GZIP_SUPPORT_DISABLED;
import static com.google.android.exoplayer2.testutil.WebServerDispatcher.Resource.GZIP_SUPPORT_FORCED;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static java.lang.Math.max; import static java.lang.Math.max;
import static java.lang.Math.min; import static java.lang.Math.min;
import android.util.Pair; import android.util.Pair;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.Dispatcher;
...@@ -41,21 +52,60 @@ public class WebServerDispatcher extends Dispatcher { ...@@ -41,21 +52,60 @@ public class WebServerDispatcher extends Dispatcher {
/** A resource served by {@link WebServerDispatcher}. */ /** A resource served by {@link WebServerDispatcher}. */
public static class Resource { public static class Resource {
/**
* The level of gzip support offered by the server for a resource.
*
* <p>One of:
*
* <ul>
* <li>{@link #GZIP_SUPPORT_DISABLED}
* <li>{@link #GZIP_SUPPORT_ENABLED}
* <li>{@link #GZIP_SUPPORT_FORCED}
* </ul>
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({GZIP_SUPPORT_DISABLED, GZIP_SUPPORT_ENABLED, GZIP_SUPPORT_FORCED})
private @interface GzipSupport {}
/** The server doesn't support gzip. */
public static final int GZIP_SUPPORT_DISABLED = 1;
/**
* The server supports gzip. Responses are only compressed if the request signals "gzip" is an
* acceptable content-coding using an {@code Accept-Encoding} header.
*/
public static final int GZIP_SUPPORT_ENABLED = 2;
/**
* The server supports gzip. Responses are compressed if the request contains no {@code
* Accept-Encoding} header or one that accepts {@code "gzip"}.
*
* <p>RFC 2616 14.3 recommends a server use {@code "identity"} content-coding if no {@code
* Accept-Encoding} is present, but some servers will still compress responses in this case.
* This option mimics that behaviour.
*/
public static final int GZIP_SUPPORT_FORCED = 3;
/** Builder for {@link Resource}. */ /** Builder for {@link Resource}. */
public static class Builder { public static class Builder {
private @MonotonicNonNull String path; private @MonotonicNonNull String path;
private byte @MonotonicNonNull [] data; private byte @MonotonicNonNull [] data;
private boolean supportsRangeRequests; private boolean supportsRangeRequests;
private boolean resolvesToUnknownLength; private boolean resolvesToUnknownLength;
@GzipSupport private int gzipSupport;
/** Constructs an instance. */ /** Constructs an instance. */
public Builder() {} public Builder() {
this.gzipSupport = GZIP_SUPPORT_DISABLED;
}
private Builder(Resource resource) { private Builder(Resource resource) {
this.path = resource.getPath(); this.path = resource.getPath();
this.data = resource.getData(); this.data = resource.getData();
this.supportsRangeRequests = resource.supportsRangeRequests(); this.supportsRangeRequests = resource.supportsRangeRequests();
this.resolvesToUnknownLength = resource.resolvesToUnknownLength(); this.resolvesToUnknownLength = resource.resolvesToUnknownLength();
this.gzipSupport = resource.getGzipSupport();
} }
/** /**
...@@ -89,7 +139,7 @@ public class WebServerDispatcher extends Dispatcher { ...@@ -89,7 +139,7 @@ public class WebServerDispatcher extends Dispatcher {
} }
/** /**
* Sets if the resource should resolve to an unknown length. Defaults to false. * Sets if the server shouldn't include the resource length in header responses.
* *
* <p>If true, responses to unbound requests won't include a Content-Length header and * <p>If true, responses to unbound requests won't include a Content-Length header and
* Content-Range headers won't include the total resource length. * Content-Range headers won't include the total resource length.
...@@ -101,10 +151,29 @@ public class WebServerDispatcher extends Dispatcher { ...@@ -101,10 +151,29 @@ public class WebServerDispatcher extends Dispatcher {
return this; return this;
} }
/**
* Sets the level of gzip support for this resource. Defaults to {@link
* #GZIP_SUPPORT_DISABLED}.
*
* @return this builder, for convenience.
*/
public Builder setGzipSupport(@GzipSupport int gzipSupport) {
this.gzipSupport = gzipSupport;
return this;
}
/** Builds the {@link Resource}. */ /** Builds the {@link Resource}. */
public Resource build() { public Resource build() {
if (gzipSupport != GZIP_SUPPORT_DISABLED) {
checkState(!supportsRangeRequests, "Can't enable compression & range requests.");
checkState(!resolvesToUnknownLength, "Can't enable compression if length isn't known.");
}
return new Resource( return new Resource(
checkNotNull(path), checkNotNull(data), supportsRangeRequests, resolvesToUnknownLength); checkNotNull(path),
checkNotNull(data),
supportsRangeRequests,
resolvesToUnknownLength,
gzipSupport);
} }
} }
...@@ -112,13 +181,19 @@ public class WebServerDispatcher extends Dispatcher { ...@@ -112,13 +181,19 @@ public class WebServerDispatcher extends Dispatcher {
private final byte[] data; private final byte[] data;
private final boolean supportsRangeRequests; private final boolean supportsRangeRequests;
private final boolean resolvesToUnknownLength; private final boolean resolvesToUnknownLength;
@GzipSupport private final int gzipSupport;
private Resource( private Resource(
String path, byte[] data, boolean supportsRangeRequests, boolean resolvesToUnknownLength) { String path,
byte[] data,
boolean supportsRangeRequests,
boolean resolvesToUnknownLength,
@GzipSupport int gzipSupport) {
this.path = path; this.path = path;
this.data = data; this.data = data;
this.supportsRangeRequests = supportsRangeRequests; this.supportsRangeRequests = supportsRangeRequests;
this.resolvesToUnknownLength = resolvesToUnknownLength; this.resolvesToUnknownLength = resolvesToUnknownLength;
this.gzipSupport = gzipSupport;
} }
/** Returns the path this resource is available at. */ /** Returns the path this resource is available at. */
...@@ -141,12 +216,22 @@ public class WebServerDispatcher extends Dispatcher { ...@@ -141,12 +216,22 @@ public class WebServerDispatcher extends Dispatcher {
return resolvesToUnknownLength; return resolvesToUnknownLength;
} }
/** Returns the level of gzip support the server should provide for this resource. */
@GzipSupport
public int getGzipSupport() {
return gzipSupport;
}
/** Returns a new {@link Builder} initialized with the values from this instance. */ /** Returns a new {@link Builder} initialized with the values from this instance. */
public Builder buildUpon() { public Builder buildUpon() {
return new Builder(this); return new Builder(this);
} }
} }
/** Matches an Accept-Encoding header value (format defined in RFC 2616 section 14.3). */
private static final Pattern ACCEPT_ENCODING_PATTERN =
Pattern.compile("\\W*(\\w+|\\*)(?:;q=(\\d+\\.?\\d*))?\\W*");
private final ImmutableMap<String, Resource> resourcesByPath; private final ImmutableMap<String, Resource> resourcesByPath;
/** /**
...@@ -171,9 +256,39 @@ public class WebServerDispatcher extends Dispatcher { ...@@ -171,9 +256,39 @@ public class WebServerDispatcher extends Dispatcher {
if (resource.supportsRangeRequests()) { if (resource.supportsRangeRequests()) {
response.setHeader("Accept-ranges", "bytes"); response.setHeader("Accept-ranges", "bytes");
} }
String rangeHeader = request.getHeader("Range"); @Nullable ImmutableMap<String, Float> acceptEncodingHeader = getAcceptEncodingHeader(request);
@Nullable String preferredContentCoding;
if (resource.getGzipSupport() == GZIP_SUPPORT_FORCED && acceptEncodingHeader == null) {
preferredContentCoding = "gzip";
} else {
ImmutableList<String> supportedContentCodings =
resource.getGzipSupport() == GZIP_SUPPORT_DISABLED
? ImmutableList.of("identity")
: ImmutableList.of("gzip", "identity");
preferredContentCoding =
getPreferredContentCoding(acceptEncodingHeader, supportedContentCodings);
}
if (preferredContentCoding == null) {
// None of the supported encodings are accepted by the client.
return response.setResponseCode(406);
}
@Nullable String rangeHeader = request.getHeader("Range");
if (!resource.supportsRangeRequests() || rangeHeader == null) { if (!resource.supportsRangeRequests() || rangeHeader == null) {
response.setBody(new Buffer().write(resourceData)); switch (preferredContentCoding) {
case "gzip":
response
.setBody(new Buffer().write(Util.gzip(resourceData)))
.setHeader("Content-Encoding", "gzip");
break;
case "identity":
response
.setBody(new Buffer().write(resourceData))
.setHeader("Content-Encoding", "identity");
break;
default:
throw new IllegalStateException("Unexpected content coding: " + preferredContentCoding);
}
if (resource.resolvesToUnknownLength()) { if (resource.resolvesToUnknownLength()) {
response.setHeader("Content-Length", ""); response.setHeader("Content-Length", "");
} }
...@@ -181,7 +296,7 @@ public class WebServerDispatcher extends Dispatcher { ...@@ -181,7 +296,7 @@ public class WebServerDispatcher extends Dispatcher {
} }
@Nullable @Nullable
Pair<@NullableType Integer, @NullableType Integer> range = parseRangeHeader(rangeHeader); Pair<@NullableType Integer, @NullableType Integer> range = getRangeHeader(rangeHeader);
if (range == null || (range.first != null && range.first >= resourceData.length)) { if (range == null || (range.first != null && range.first >= resourceData.length)) {
return response return response
...@@ -244,10 +359,82 @@ public class WebServerDispatcher extends Dispatcher { ...@@ -244,10 +359,82 @@ public class WebServerDispatcher extends Dispatcher {
} }
/** /**
* Parses an RFC 2616 14.3 Accept-Encoding header into a map from content-coding to qvalue.
*
* <p>Returns null if the header is not present.
*
* <p>Missing qvalues are stored in the map as -1.
*/
@Nullable
private static ImmutableMap<String, Float> getAcceptEncodingHeader(RecordedRequest request) {
@Nullable List<String> headers = request.getHeaders().toMultimap().get("Accept-Encoding");
if (headers == null) {
return null;
}
String header = Joiner.on(",").join(headers);
String[] encodings = Util.split(header, ",");
ImmutableMap.Builder<String, Float> parsedEncodings = ImmutableMap.builder();
for (String encoding : encodings) {
Matcher matcher = ACCEPT_ENCODING_PATTERN.matcher(encoding);
if (!matcher.matches()) {
continue;
}
String contentCoding = checkNotNull(matcher.group(1));
@Nullable String qvalue = matcher.group(2);
parsedEncodings.put(contentCoding, qvalue == null ? -1f : Float.parseFloat(qvalue));
}
return parsedEncodings.build();
}
/**
* Returns the preferred content-coding based on the (optional) Accept-Encoding header, or null if
* none of {@code supportedContentCodings} are accepted by the client.
*
* <p>The selection algorithm is described in RFC 2616 section 14.3.
*
* @param acceptEncodingHeader The Accept-Encoding header parsed into a map from content-coding to
* qvalue (absent qvalues are represented by -1), or null if the header isn't present.
* @param supportedContentCodings A list of content-codings supported by the server in order of
* preference.
*/
@Nullable
private static String getPreferredContentCoding(
@Nullable ImmutableMap<String, Float> acceptEncodingHeader,
List<String> supportedContentCodings) {
if (acceptEncodingHeader == null) {
return "identity";
}
if (!acceptEncodingHeader.containsKey("identity") && !acceptEncodingHeader.containsKey("*")) {
acceptEncodingHeader =
ImmutableMap.<String, Float>builder()
.putAll(acceptEncodingHeader)
.put("identity", -1f)
.build();
}
float asteriskQvalue = acceptEncodingHeader.getOrDefault("*", 0f);
@Nullable String preferredContentCoding = null;
float preferredQvalue = Integer.MIN_VALUE;
for (String supportedContentCoding : supportedContentCodings) {
float qvalue = acceptEncodingHeader.getOrDefault(supportedContentCoding, 0f);
if (!acceptEncodingHeader.containsKey(supportedContentCoding)
&& asteriskQvalue != 0
&& asteriskQvalue > preferredQvalue) {
preferredContentCoding = supportedContentCoding;
preferredQvalue = asteriskQvalue;
} else if (qvalue != 0 && qvalue > preferredQvalue) {
preferredContentCoding = supportedContentCoding;
preferredQvalue = qvalue;
}
}
return preferredContentCoding;
}
/**
* Parses an RFC 7233 Range header to its component parts. Returns null if the Range is invalid. * Parses an RFC 7233 Range header to its component parts. Returns null if the Range is invalid.
*/ */
@Nullable @Nullable
private static Pair<@NullableType Integer, @NullableType Integer> parseRangeHeader( private static Pair<@NullableType Integer, @NullableType Integer> getRangeHeader(
String rangeHeader) { String rangeHeader) {
Pattern rangePattern = Pattern.compile("bytes=(\\d*)-(\\d*)"); Pattern rangePattern = Pattern.compile("bytes=(\\d*)-(\\d*)");
Matcher rangeMatcher = rangePattern.matcher(rangeHeader); Matcher rangeMatcher = rangePattern.matcher(rangeHeader);
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.testutil; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.testutil;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.Arrays; import java.util.Arrays;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
...@@ -29,6 +30,26 @@ import org.junit.Test; ...@@ -29,6 +30,26 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
/** Tests for {@link WebServerDispatcher}. */ /** Tests for {@link WebServerDispatcher}. */
// We use the OkHttp client library for these tests because it's generally nicer to use than Java's
// HttpURLConnection.
//
// However, OkHttp's 'transparent compression' behaviour is annoying when trying to test the edge
// cases of the WebServerDispatcher's Accept-Encoding header handling. If passed a request with no
// Accept-Encoding header, the OkHttp client library will silently add one that accepts gzip and
// then silently unzip the response data (and remove the Content-Coding header) before returning it.
//
// This gets in the way of some test cases, for example testing how the WebServerDispatcher handles
// a request with *no* Accept-Encoding header (since it's impossible to send this using OkHttp).
//
// Under Robolectric, the Java HttpURLConnection doesn't have this transparent compression
// behaviour, but that's a Robolectric 'bug' (internal: b/177071755) because the Android platform
// implementation of HttpURLConnection does (it uses OkHttp under the hood). So we can't really use
// HttpURLConnection to test these edge cases either (even though it would work for now) because
// ideally Robolectric will in the future make the implementation more realistic and suddenly our
// tests would be wrong.
//
// So instead we just don't test these cases that require passing header combinations that are
// impossible with OkHttp.
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class WebServerDispatcherTest { public class WebServerDispatcherTest {
...@@ -49,6 +70,10 @@ public class WebServerDispatcherTest { ...@@ -49,6 +70,10 @@ public class WebServerDispatcherTest {
"/range/requests/not-supported-length-unknown"; "/range/requests/not-supported-length-unknown";
private static final byte[] RANGE_UNSUPPORTED_LENGTH_UNKNOWN_DATA = private static final byte[] RANGE_UNSUPPORTED_LENGTH_UNKNOWN_DATA =
TestUtil.buildTestData(/* length= */ 20, seed++); TestUtil.buildTestData(/* length= */ 20, seed++);
private static final String GZIP_ENABLED_PATH = "/gzip/enabled";
private static final byte[] GZIP_ENABLED_DATA = TestUtil.buildTestData(/* length= */ 20, seed++);
private static final String GZIP_FORCED_PATH = "/gzip/forced";
private static final byte[] GZIP_FORCED_DATA = TestUtil.buildTestData(/* length= */ 20, seed++);
private MockWebServer mockWebServer; private MockWebServer mockWebServer;
...@@ -79,6 +104,16 @@ public class WebServerDispatcherTest { ...@@ -79,6 +104,16 @@ public class WebServerDispatcherTest {
.setData(RANGE_UNSUPPORTED_LENGTH_UNKNOWN_DATA) .setData(RANGE_UNSUPPORTED_LENGTH_UNKNOWN_DATA)
.supportsRangeRequests(false) .supportsRangeRequests(false)
.resolvesToUnknownLength(true) .resolvesToUnknownLength(true)
.build(),
new WebServerDispatcher.Resource.Builder()
.setPath(GZIP_ENABLED_PATH)
.setData(GZIP_ENABLED_DATA)
.setGzipSupport(WebServerDispatcher.Resource.GZIP_SUPPORT_ENABLED)
.build(),
new WebServerDispatcher.Resource.Builder()
.setPath(GZIP_FORCED_PATH)
.setData(GZIP_FORCED_DATA)
.setGzipSupport(WebServerDispatcher.Resource.GZIP_SUPPORT_FORCED)
.build()))); .build())));
} }
...@@ -392,4 +427,155 @@ public class WebServerDispatcherTest { ...@@ -392,4 +427,155 @@ public class WebServerDispatcherTest {
assertThat(response.body().bytes()).isEqualTo(RANGE_UNSUPPORTED_DATA); assertThat(response.body().bytes()).isEqualTo(RANGE_UNSUPPORTED_DATA);
} }
} }
@Test
public void gzipDisabled_acceptEncodingHeaderAllowsAnyCoding_identityResponse() throws Exception {
OkHttpClient client = new OkHttpClient();
Request request =
new Request.Builder()
.url(mockWebServer.url(RANGE_SUPPORTED_PATH))
.addHeader("Accept-Encoding", "*")
.build();
try (Response response = client.newCall(request).execute()) {
assertThat(response.code()).isEqualTo(200);
assertThat(response.header("Content-Encoding")).isEqualTo("identity");
assertThat(response.header("Content-Length"))
.isEqualTo(String.valueOf(RANGE_SUPPORTED_DATA.length));
assertThat(response.body().bytes()).isEqualTo(RANGE_SUPPORTED_DATA);
}
}
@Test
public void gzipDisabled_acceptEncodingHeaderRequiresGzip_406Response() throws Exception {
OkHttpClient client = new OkHttpClient();
Request request =
new Request.Builder()
.url(mockWebServer.url(RANGE_SUPPORTED_PATH))
.addHeader("Accept-Encoding", "gzip;q=1.0")
.addHeader("Accept-Encoding", "identity;q=0")
.build();
try (Response response = client.newCall(request).execute()) {
assertThat(response.code()).isEqualTo(406);
}
}
@Test
public void gzipDisabled_acceptEncodingHeaderRequiresGzipViaAsterisk_406Response()
throws Exception {
OkHttpClient client = new OkHttpClient();
Request request =
new Request.Builder()
.url(mockWebServer.url(RANGE_SUPPORTED_PATH))
.addHeader("Accept-Encoding", "gzip;q=1.0")
.addHeader("Accept-Encoding", "*;q=0")
.build();
try (Response response = client.newCall(request).execute()) {
assertThat(response.code()).isEqualTo(406);
}
}
@Test
public void gzipEnabled_acceptEncodingHeaderAllowsGzip_responseGzipped() throws Exception {
OkHttpClient client = new OkHttpClient();
Request request =
new Request.Builder()
.url(mockWebServer.url(GZIP_ENABLED_PATH))
.addHeader("Accept-Encoding", "gzip")
.build();
try (Response response = client.newCall(request).execute()) {
byte[] expectedData = Util.gzip(GZIP_ENABLED_DATA);
assertThat(response.code()).isEqualTo(200);
assertThat(response.header("Content-Encoding")).isEqualTo("gzip");
assertThat(response.header("Content-Length")).isEqualTo(String.valueOf(expectedData.length));
assertThat(response.body().bytes()).isEqualTo(expectedData);
}
}
@Test
public void gzipEnabled_acceptEncodingHeaderAllowsAnyCoding_responseGzipped() throws Exception {
OkHttpClient client = new OkHttpClient();
Request request =
new Request.Builder()
.url(mockWebServer.url(GZIP_ENABLED_PATH))
.addHeader("Accept-Encoding", "*")
.build();
try (Response response = client.newCall(request).execute()) {
byte[] expectedData = Util.gzip(GZIP_ENABLED_DATA);
assertThat(response.code()).isEqualTo(200);
assertThat(response.header("Content-Encoding")).isEqualTo("gzip");
assertThat(response.header("Content-Length")).isEqualTo(String.valueOf(expectedData.length));
assertThat(response.body().bytes()).isEqualTo(expectedData);
}
}
@Test
public void gzipEnabled_acceptEncodingHeaderPrefersIdentity_responseNotGzipped()
throws Exception {
OkHttpClient client = new OkHttpClient();
Request request =
new Request.Builder()
.url(mockWebServer.url(GZIP_ENABLED_PATH))
.addHeader("Accept-Encoding", "identity;q=0.8, gzip;q=0.2")
.build();
try (Response response = client.newCall(request).execute()) {
assertThat(response.code()).isEqualTo(200);
assertThat(response.header("Content-Encoding")).isEqualTo("identity");
assertThat(response.header("Content-Length"))
.isEqualTo(String.valueOf(GZIP_ENABLED_DATA.length));
assertThat(response.body().bytes()).isEqualTo(GZIP_ENABLED_DATA);
}
}
@Test
public void gzipEnabled_acceptEncodingHeaderExcludesGzip_responseNotGzipped() throws Exception {
OkHttpClient client = new OkHttpClient();
Request request =
new Request.Builder()
.url(mockWebServer.url(GZIP_ENABLED_PATH))
.addHeader("Accept-Encoding", "identity")
.build();
try (Response response = client.newCall(request).execute()) {
assertThat(response.code()).isEqualTo(200);
assertThat(response.header("Content-Encoding")).isEqualTo("identity");
assertThat(response.header("Content-Length"))
.isEqualTo(String.valueOf(GZIP_ENABLED_DATA.length));
assertThat(response.body().bytes()).isEqualTo(GZIP_ENABLED_DATA);
}
}
@Test
public void gzipForced_acceptEncodingHeaderAllowsGzip_responseGzipped() throws Exception {
OkHttpClient client = new OkHttpClient();
Request request =
new Request.Builder()
.url(mockWebServer.url(GZIP_FORCED_PATH))
.addHeader("Accept-Encoding", "gzip")
.build();
try (Response response = client.newCall(request).execute()) {
byte[] expectedData = Util.gzip(GZIP_FORCED_DATA);
assertThat(response.code()).isEqualTo(200);
assertThat(response.header("Content-Encoding")).isEqualTo("gzip");
assertThat(response.header("Content-Length")).isEqualTo(String.valueOf(expectedData.length));
assertThat(response.body().bytes()).isEqualTo(expectedData);
}
}
@Test
public void gzipForced_acceptEncodingHeaderExcludesGzip_responseNotGzipped() throws Exception {
OkHttpClient client = new OkHttpClient();
Request request =
new Request.Builder()
.url(mockWebServer.url(GZIP_FORCED_PATH))
.addHeader("Accept-Encoding", "identity")
.build();
try (Response response = client.newCall(request).execute()) {
assertThat(response.code()).isEqualTo(200);
assertThat(response.header("Content-Encoding")).isEqualTo("identity");
assertThat(response.header("Content-Length"))
.isEqualTo(String.valueOf(GZIP_FORCED_DATA.length));
assertThat(response.body().bytes()).isEqualTo(GZIP_FORCED_DATA);
}
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment