Introduce FakeExtractorInput (based loosely on RecordableExtractorInput).

- Use it to simplify a bunch of tests.
- Will also replace RecordableExtractorInput in a subsequent CL.
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=117220030
This commit is contained in:
olly 2016-03-15 02:44:24 -07:00 committed by Oliver Woodman
parent a1a48abe92
commit 300b58e530
5 changed files with 306 additions and 96 deletions

View File

@ -15,14 +15,9 @@
*/ */
package com.google.android.exoplayer.extractor.webm; package com.google.android.exoplayer.extractor.webm;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.extractor.DefaultExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorInput; import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.testutil.FakeDataSource; import com.google.android.exoplayer.testutil.FakeExtractorInput;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.testutil.TestUtil;
import com.google.android.exoplayer.upstream.DataSpec;
import android.net.Uri;
import junit.framework.TestCase; import junit.framework.TestCase;
@ -140,22 +135,15 @@ public class DefaultEbmlReaderTest extends TestCase {
/** /**
* Helper to build an {@link ExtractorInput} from byte data. * Helper to build an {@link ExtractorInput} from byte data.
* <p>
* Each argument must be able to cast to a byte value.
* *
* @param data Zero or more integers with values between {@code 0x00} and {@code 0xFF}. * @param data Zero or more integers with values between {@code 0x00} and {@code 0xFF}.
* @return An {@link ExtractorInput} from which the data can be read. * @return An {@link ExtractorInput} from which the data can be read.
* @throws IOException If an error occurs creating the input.
*/ */
private static ExtractorInput createTestInput(int... data) throws IOException { private static ExtractorInput createTestInput(int... data) {
byte[] bytes = new byte[data.length]; return new FakeExtractorInput.Builder()
for (int i = 0; i < data.length; i++) { .setData(TestUtil.createByteArray(data))
bytes[i] = (byte) data[i]; .setSimulateUnknownLength(true)
} .build();
DataSource dataSource = new FakeDataSource.Builder().appendReadData(bytes).build();
dataSource.open(new DataSpec(Uri.parse("http://www.google.com")));
ExtractorInput input = new DefaultExtractorInput(dataSource, 0, C.LENGTH_UNBOUNDED);
return input;
} }
/** /**

View File

@ -16,26 +16,20 @@
package com.google.android.exoplayer.extractor.webm; package com.google.android.exoplayer.extractor.webm;
import com.google.android.exoplayer.C; import com.google.android.exoplayer.C;
import com.google.android.exoplayer.extractor.DefaultExtractorInput;
import com.google.android.exoplayer.extractor.ExtractorInput; import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.testutil.FakeDataSource; import com.google.android.exoplayer.testutil.FakeExtractorInput;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.testutil.FakeExtractorInput.SimulatedIOException;
import com.google.android.exoplayer.upstream.DataSpec;
import android.net.Uri;
import junit.framework.TestCase; import junit.framework.TestCase;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
/** /**
* Tests for {@link VarintReader}. * Tests for {@link VarintReader}.
*/ */
public class VarintReaderTest extends TestCase { public final class VarintReaderTest extends TestCase {
private static final String TEST_URI = "http://www.google.com";
private static final byte MAX_BYTE = (byte) 0xFF; private static final byte MAX_BYTE = (byte) 0xFF;
private static final byte[] DATA_1_BYTE_0 = new byte[] {(byte) 0x80}; private static final byte[] DATA_1_BYTE_0 = new byte[] {(byte) 0x80};
@ -88,12 +82,10 @@ public class VarintReaderTest extends TestCase {
public void testReadVarintEndOfInputAtStart() throws IOException, InterruptedException { public void testReadVarintEndOfInputAtStart() throws IOException, InterruptedException {
VarintReader reader = new VarintReader(); VarintReader reader = new VarintReader();
// Build an input, and read to the end. // Build an input with no data.
DataSource dataSource = buildDataSource(new byte[1]); ExtractorInput input = new FakeExtractorInput.Builder()
dataSource.open(new DataSpec(Uri.parse(TEST_URI))); .setSimulateUnknownLength(true)
ExtractorInput input = new DefaultExtractorInput(dataSource, 0, C.LENGTH_UNBOUNDED); .build();
int bytesRead = input.read(new byte[1], 0, 1);
assertEquals(1, bytesRead);
// End of input allowed. // End of input allowed.
long result = reader.readUnsignedVarint(input, true, false, 8); long result = reader.readUnsignedVarint(input, true, false, 8);
assertEquals(C.RESULT_END_OF_INPUT, result); assertEquals(C.RESULT_END_OF_INPUT, result);
@ -108,9 +100,10 @@ public class VarintReaderTest extends TestCase {
public void testReadVarintExceedsMaximumAllowedLength() throws IOException, InterruptedException { public void testReadVarintExceedsMaximumAllowedLength() throws IOException, InterruptedException {
VarintReader reader = new VarintReader(); VarintReader reader = new VarintReader();
DataSource dataSource = buildDataSource(DATA_8_BYTE_0); ExtractorInput input = new FakeExtractorInput.Builder()
dataSource.open(new DataSpec(Uri.parse(TEST_URI))); .setData(DATA_8_BYTE_0)
ExtractorInput input = new DefaultExtractorInput(dataSource, 0, C.LENGTH_UNBOUNDED); .setSimulateUnknownLength(true)
.build();
long result = reader.readUnsignedVarint(input, false, true, 4); long result = reader.readUnsignedVarint(input, false, true, 4);
assertEquals(C.RESULT_MAX_LENGTH_EXCEEDED, result); assertEquals(C.RESULT_MAX_LENGTH_EXCEEDED, result);
} }
@ -189,9 +182,10 @@ public class VarintReaderTest extends TestCase {
private static void testReadVarint(VarintReader reader, boolean removeMask, byte[] data, private static void testReadVarint(VarintReader reader, boolean removeMask, byte[] data,
int expectedLength, long expectedValue) throws IOException, InterruptedException { int expectedLength, long expectedValue) throws IOException, InterruptedException {
DataSource dataSource = buildDataSource(data); ExtractorInput input = new FakeExtractorInput.Builder()
dataSource.open(new DataSpec(Uri.parse(TEST_URI))); .setData(data)
ExtractorInput input = new DefaultExtractorInput(dataSource, 0, C.LENGTH_UNBOUNDED); .setSimulateUnknownLength(true)
.build();
long result = reader.readUnsignedVarint(input, false, removeMask, 8); long result = reader.readUnsignedVarint(input, false, removeMask, 8);
assertEquals(expectedLength, input.getPosition()); assertEquals(expectedLength, input.getPosition());
assertEquals(expectedValue, result); assertEquals(expectedValue, result);
@ -199,45 +193,22 @@ public class VarintReaderTest extends TestCase {
private static void testReadVarintFlaky(VarintReader reader, boolean removeMask, byte[] data, private static void testReadVarintFlaky(VarintReader reader, boolean removeMask, byte[] data,
int expectedLength, long expectedValue) throws IOException, InterruptedException { int expectedLength, long expectedValue) throws IOException, InterruptedException {
DataSource dataSource = buildFlakyDataSource(data); ExtractorInput input = new FakeExtractorInput.Builder()
ExtractorInput input = null; .setData(data)
long position = 0; .setSimulateUnknownLength(true)
.setSimulateIOErrors(true)
.setSimulatePartialReads(true)
.build();
long result = -1; long result = -1;
while (result == -1) { while (result == -1) {
dataSource.open(new DataSpec(Uri.parse(TEST_URI), position, C.LENGTH_UNBOUNDED, null));
input = new DefaultExtractorInput(dataSource, position, C.LENGTH_UNBOUNDED);
try { try {
result = reader.readUnsignedVarint(input, false, removeMask, 8); result = reader.readUnsignedVarint(input, false, removeMask, 8);
position = input.getPosition(); } catch (SimulatedIOException e) {
} catch (IOException e) { // Expected.
// Expected. We'll try again from the position that the input was advanced to.
position = input.getPosition();
dataSource.close();
} }
} }
assertEquals(expectedLength, input.getPosition()); assertEquals(expectedLength, input.getPosition());
assertEquals(expectedValue, result); assertEquals(expectedValue, result);
} }
private static DataSource buildDataSource(byte[] data) {
FakeDataSource.Builder builder = new FakeDataSource.Builder();
builder.appendReadData(data);
return builder.build();
}
private static DataSource buildFlakyDataSource(byte[] data) {
FakeDataSource.Builder builder = new FakeDataSource.Builder();
builder.appendReadError(new IOException("A"));
builder.appendReadData(new byte[] {data[0]});
if (data.length > 1) {
builder.appendReadError(new IOException("B"));
builder.appendReadData(new byte[] {data[1]});
}
if (data.length > 2) {
builder.appendReadError(new IOException("C"));
builder.appendReadData(Arrays.copyOfRange(data, 2, data.length));
}
return builder.build();
}
} }

View File

@ -48,7 +48,7 @@ public final class FakeDataSource implements DataSource {
private int currentSegmentIndex; private int currentSegmentIndex;
private long bytesRemaining; private long bytesRemaining;
public FakeDataSource(boolean simulateUnknownLength, ArrayList<Segment> segments) { private FakeDataSource(boolean simulateUnknownLength, ArrayList<Segment> segments) {
this.simulateUnknownLength = simulateUnknownLength; this.simulateUnknownLength = simulateUnknownLength;
this.segments = segments; this.segments = segments;
long totalLength = 0; long totalLength = 0;
@ -163,7 +163,7 @@ public final class FakeDataSource implements DataSource {
/** /**
* Builder of {@link FakeDataSource} instances. * Builder of {@link FakeDataSource} instances.
*/ */
public static class Builder { public static final class Builder {
private final ArrayList<Segment> segments; private final ArrayList<Segment> segments;
private boolean simulateUnknownLength; private boolean simulateUnknownLength;

View File

@ -0,0 +1,270 @@
/*
* Copyright (C) 2015 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.exoplayer.testutil;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.extractor.ExtractorInput;
import android.util.SparseBooleanArray;
import junit.framework.Assert;
import java.io.EOFException;
import java.io.IOException;
/**
* A fake {@link ExtractorInput} capable of simulating various scenarios.
* <p>
* Read, skip and peek errors can be simulated using {@link Builder#setSimulateIOErrors)}. When
* enabled each read and skip will throw a {@link SimulatedIOException} unless one has already been
* thrown from the current position. Each peek will throw {@link SimulatedIOException} unless one
* has already been thrown from the current peek position. When a {@link SimulatedIOException} is
* thrown the read position is left unchanged and the peek position is reset back to the read
* position.
* <p>
* Partial reads and skips can be simulated using {@link Builder#setSimulatePartialReads}. When
* enabled, {@link #read(byte[], int, int)} and {@link #skip(int)} calls will only read or skip a
* single byte unless a partial read or skip has already been performed that had the same target
* position. For example, a first read request for 10 bytes will be partially satisfied by reading
* a single byte and advancing the position to 1. If the following read request attempts to read 9
* bytes then it will be fully satisfied, since it has the same target position of 10.
* <p>
* Unknown data length can be simulated using {@link Builder#setSimulateUnknownLength}. When enabled
* {@link getLength()} will return {@link C#LENGTH_UNBOUNDED} rather than the length of the data.
*/
public final class FakeExtractorInput implements ExtractorInput {
/**
* Thrown when simulating an {@link IOException}.
*/
public static final class SimulatedIOException extends IOException {
public SimulatedIOException(String message) {
super(message);
}
}
private final byte[] data;
private final boolean simulateUnknownLength;
private final boolean simulatePartialReads;
private final boolean simulateIOErrors;
private int readPosition;
private int peekPosition;
private final SparseBooleanArray partiallySatisfiedTargetPositions;
private final SparseBooleanArray failedReadPositions;
private final SparseBooleanArray failedPeekPositions;
private FakeExtractorInput(byte[] data, boolean simulateUnknownLength,
boolean simulatePartialReads, boolean simulateIOErrors) {
this.data = data;
this.simulateUnknownLength = simulateUnknownLength;
this.simulatePartialReads = simulatePartialReads;
this.simulateIOErrors = simulateIOErrors;
partiallySatisfiedTargetPositions = new SparseBooleanArray();
failedReadPositions = new SparseBooleanArray();
failedPeekPositions = new SparseBooleanArray();
}
/**
* Sets the read and peek positions.
*
* @param position The position to set.
*/
public void setPosition(int position) {
Assert.assertTrue(0 <= position && position < data.length);
readPosition = position;
peekPosition = position;
}
@Override
public int read(byte[] target, int offset, int length) throws IOException {
length = getReadLength(length);
if (readFully(target, offset, length, true)) {
return length;
}
return C.RESULT_END_OF_INPUT;
}
@Override
public boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput)
throws IOException {
if (!checkXFully(allowEndOfInput, readPosition, length, failedReadPositions)) {
return false;
}
System.arraycopy(data, readPosition, target, offset, length);
readPosition += length;
peekPosition = readPosition;
return true;
}
@Override
public void readFully(byte[] target, int offset, int length) throws IOException {
readFully(target, offset, length, false);
}
@Override
public int skip(int length) throws IOException {
length = getReadLength(length);
if (skipFully(length, true)) {
return length;
}
return C.RESULT_END_OF_INPUT;
}
@Override
public boolean skipFully(int length, boolean allowEndOfInput) throws IOException {
if (!checkXFully(allowEndOfInput, readPosition, length, failedReadPositions)) {
return false;
}
readPosition += length;
peekPosition = readPosition;
return true;
}
@Override
public void skipFully(int length) throws IOException {
skipFully(length, false);
}
@Override
public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput)
throws IOException {
if (!checkXFully(allowEndOfInput, peekPosition, length, failedPeekPositions)) {
return false;
}
System.arraycopy(data, peekPosition, target, offset, length);
peekPosition += length;
return true;
}
@Override
public void peekFully(byte[] target, int offset, int length) throws IOException {
peekFully(target, offset, length, false);
}
@Override
public boolean advancePeekPosition(int length, boolean allowEndOfInput) throws IOException {
if (!checkXFully(allowEndOfInput, peekPosition, length, failedPeekPositions)) {
return false;
}
peekPosition += length;
return true;
}
@Override
public void advancePeekPosition(int length) throws IOException {
advancePeekPosition(length, false);
}
@Override
public void resetPeekPosition() {
peekPosition = readPosition;
}
@Override
public long getPeekPosition() {
return peekPosition;
}
@Override
public long getPosition() {
return readPosition;
}
@Override
public long getLength() {
return simulateUnknownLength ? C.LENGTH_UNBOUNDED : data.length;
}
private boolean checkXFully(boolean allowEndOfInput, int position, int length,
SparseBooleanArray failedPositions) throws IOException {
if (simulateIOErrors && !failedPositions.get(position)) {
failedPositions.put(position, true);
peekPosition = readPosition;
throw new SimulatedIOException("Simulated IO error at position: " + position);
}
if (isEof()) {
if (allowEndOfInput) {
return false;
}
throw new EOFException();
}
if (position + length > data.length) {
throw new IOException("Attempted to move past end of data: (" + position + " + "
+ length + ") > " + data.length);
}
return true;
}
private int getReadLength(int requestedLength) {
int targetPosition = readPosition + requestedLength;
if (simulatePartialReads && requestedLength > 1
&& !partiallySatisfiedTargetPositions.get(targetPosition)) {
partiallySatisfiedTargetPositions.put(targetPosition, true);
return 1;
}
return Math.min(requestedLength, data.length - readPosition);
}
private boolean isEof() {
return readPosition == data.length;
}
/**
* Builder of {@link FakeExtractorInput} instances.
*/
public static final class Builder {
private byte[] data;
private boolean simulateUnknownLength;
private boolean simulatePartialReads;
private boolean simulateIOErrors;
public Builder() {
data = new byte[0];
}
public Builder setData(byte[] data) {
this.data = data;
return this;
}
public Builder setSimulateUnknownLength(boolean simulateUnknownLength) {
this.simulateUnknownLength = simulateUnknownLength;
return this;
}
public Builder setSimulatePartialReads(boolean simulatePartialReads) {
this.simulatePartialReads = simulatePartialReads;
return this;
}
public Builder setSimulateIOErrors(boolean simulateIOErrors) {
this.simulateIOErrors = simulateIOErrors;
return this;
}
public FakeExtractorInput build() {
return new FakeExtractorInput(data, simulateUnknownLength, simulatePartialReads,
simulateIOErrors);
}
}
}

View File

@ -15,24 +15,18 @@
*/ */
package com.google.android.exoplayer.testutil; package com.google.android.exoplayer.testutil;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.extractor.DefaultExtractorInput;
import com.google.android.exoplayer.extractor.Extractor; import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorInput;
import com.google.android.exoplayer.extractor.PositionHolder; import com.google.android.exoplayer.extractor.PositionHolder;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
import android.app.Instrumentation; import android.app.Instrumentation;
import android.net.Uri;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Arrays;
import java.util.Random; import java.util.Random;
/** /**
@ -44,32 +38,19 @@ public class TestUtil {
public static void consumeTestData(Extractor extractor, byte[] data) public static void consumeTestData(Extractor extractor, byte[] data)
throws IOException, InterruptedException { throws IOException, InterruptedException {
ExtractorInput input = createTestExtractorInput(data); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
PositionHolder seekPositionHolder = new PositionHolder(); PositionHolder seekPositionHolder = new PositionHolder();
int readResult = Extractor.RESULT_CONTINUE; int readResult = Extractor.RESULT_CONTINUE;
while (readResult != Extractor.RESULT_END_OF_INPUT) { while (readResult != Extractor.RESULT_END_OF_INPUT) {
readResult = extractor.read(input, seekPositionHolder); readResult = extractor.read(input, seekPositionHolder);
if (readResult == Extractor.RESULT_SEEK) { if (readResult == Extractor.RESULT_SEEK) {
input = createTestExtractorInput(data, (int) seekPositionHolder.position); long seekPosition = seekPositionHolder.position;
Assertions.checkState(0 < seekPosition && seekPosition <= Integer.MAX_VALUE);
input.setPosition((int) seekPosition);
} }
} }
} }
public static ExtractorInput createTestExtractorInput(byte[] data) throws IOException {
return createTestExtractorInput(data, 0);
}
public static ExtractorInput createTestExtractorInput(byte[] data, int offset)
throws IOException {
if (offset != 0) {
data = Arrays.copyOfRange(data, offset, data.length);
}
FakeDataSource dataSource = new FakeDataSource.Builder().appendReadData(data).build();
dataSource.open(new DataSpec(Uri.parse("http://www.google.com")));
ExtractorInput input = new DefaultExtractorInput(dataSource, offset, C.LENGTH_UNBOUNDED);
return input;
}
public static byte[] buildTestData(int length) { public static byte[] buildTestData(int length) {
return buildTestData(length, length); return buildTestData(length, length);
} }