/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.testutil;
import android.app.Instrumentation;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Random;
import junit.framework.Assert;
import org.mockito.MockitoAnnotations;
/**
* Utility methods for tests.
*/
public class TestUtil {
/**
* A factory for {@link Extractor} instances.
*/
public interface ExtractorFactory {
Extractor create();
}
private static final String DUMP_EXTENSION = ".dump";
private static final String UNKNOWN_LENGTH_EXTENSION = ".unklen" + DUMP_EXTENSION;
private TestUtil() {}
public static boolean sniffTestData(Extractor extractor, byte[] data)
throws IOException, InterruptedException {
return sniffTestData(extractor, newExtractorInput(data));
}
public static boolean sniffTestData(Extractor extractor, FakeExtractorInput input)
throws IOException, InterruptedException {
while (true) {
try {
return extractor.sniff(input);
} catch (SimulatedIOException e) {
// Ignore.
}
}
}
public static FakeExtractorOutput consumeTestData(Extractor extractor, FakeExtractorInput input,
long timeUs) throws IOException, InterruptedException {
return consumeTestData(extractor, input, timeUs, false);
}
public static FakeExtractorOutput consumeTestData(Extractor extractor, FakeExtractorInput input,
long timeUs, boolean retryFromStartIfLive) throws IOException, InterruptedException {
FakeExtractorOutput output = new FakeExtractorOutput();
extractor.init(output);
consumeTestData(extractor, input, timeUs, output, retryFromStartIfLive);
return output;
}
private static void consumeTestData(Extractor extractor, FakeExtractorInput input, long timeUs,
FakeExtractorOutput output, boolean retryFromStartIfLive)
throws IOException, InterruptedException {
extractor.seek(input.getPosition(), timeUs);
PositionHolder seekPositionHolder = new PositionHolder();
int readResult = Extractor.RESULT_CONTINUE;
while (readResult != Extractor.RESULT_END_OF_INPUT) {
try {
// Extractor.read should not read seekPositionHolder.position. Set it to a value that's
// likely to cause test failure if a read does occur.
seekPositionHolder.position = Long.MIN_VALUE;
readResult = extractor.read(input, seekPositionHolder);
if (readResult == Extractor.RESULT_SEEK) {
long seekPosition = seekPositionHolder.position;
Assertions.checkState(0 <= seekPosition && seekPosition <= Integer.MAX_VALUE);
input.setPosition((int) seekPosition);
}
} catch (SimulatedIOException e) {
if (!retryFromStartIfLive) {
continue;
}
boolean isOnDemand = input.getLength() != C.LENGTH_UNSET
|| (output.seekMap != null && output.seekMap.getDurationUs() != C.TIME_UNSET);
if (isOnDemand) {
continue;
}
input.setPosition(0);
for (int i = 0; i < output.numberOfTracks; i++) {
output.trackOutputs.valueAt(i).clear();
}
extractor.seek(0, 0);
}
}
}
public static byte[] buildTestData(int length) {
return buildTestData(length, length);
}
public static byte[] buildTestData(int length, int seed) {
return buildTestData(length, new Random(seed));
}
public static byte[] buildTestData(int length, Random random) {
byte[] source = new byte[length];
random.nextBytes(source);
return source;
}
public static String buildTestString(int maxLength, Random random) {
int length = random.nextInt(maxLength);
StringBuilder builder = new StringBuilder(length);
for (int i = 0; i < length; i++) {
builder.append((char) random.nextInt());
}
return builder.toString();
}
/**
* Converts an array of integers in the range [0, 255] into an equivalent byte array.
*
* @param intArray An array of integers, all of which must be in the range [0, 255].
* @return The equivalent byte array.
*/
public static byte[] createByteArray(int... intArray) {
byte[] byteArray = new byte[intArray.length];
for (int i = 0; i < byteArray.length; i++) {
Assertions.checkState(0x00 <= intArray[i] && intArray[i] <= 0xFF);
byteArray[i] = (byte) intArray[i];
}
return byteArray;
}
public static byte[] joinByteArrays(byte[]... byteArrays) {
int length = 0;
for (byte[] byteArray : byteArrays) {
length += byteArray.length;
}
byte[] joined = new byte[length];
length = 0;
for (byte[] byteArray : byteArrays) {
System.arraycopy(byteArray, 0, joined, length, byteArray.length);
length += byteArray.length;
}
return joined;
}
public static void setUpMockito(InstrumentationTestCase instrumentationTestCase) {
// Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2.
System.setProperty("dexmaker.dexcache",
instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath());
MockitoAnnotations.initMocks(instrumentationTestCase);
}
public static boolean assetExists(Instrumentation instrumentation, String fileName)
throws IOException {
int i = fileName.lastIndexOf('/');
String path = i >= 0 ? fileName.substring(0, i) : "";
String file = i >= 0 ? fileName.substring(i + 1) : fileName;
return Arrays.asList(instrumentation.getContext().getResources().getAssets().list(path))
.contains(file);
}
public static byte[] getByteArray(Instrumentation instrumentation, String fileName)
throws IOException {
return Util.toByteArray(getInputStream(instrumentation, fileName));
}
public static InputStream getInputStream(Instrumentation instrumentation, String fileName)
throws IOException {
return instrumentation.getContext().getResources().getAssets().open(fileName);
}
public static String getString(Instrumentation instrumentation, String fileName)
throws IOException {
return new String(getByteArray(instrumentation, fileName));
}
private static FakeExtractorInput newExtractorInput(byte[] data) {
return new FakeExtractorInput.Builder().setData(data).build();
}
/**
* Calls {@link #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean,
* boolean)} with all possible combinations of "simulate" parameters.
*
* @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor}
* class which is to be tested.
* @param sampleFile The path to the input sample.
* @param instrumentation To be used to load the sample file.
* @throws IOException If reading from the input fails.
* @throws InterruptedException If interrupted while reading from the input.
* @see #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, boolean)
*/
public static void assertOutput(ExtractorFactory factory, String sampleFile,
Instrumentation instrumentation) throws IOException, InterruptedException {
byte[] fileData = getByteArray(instrumentation, sampleFile);
assertOutput(factory, sampleFile, fileData, instrumentation);
}
/**
* Calls {@link #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean,
* boolean)} with all possible combinations of "simulate" parameters.
*
* @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor}
* class which is to be tested.
* @param sampleFile The path to the input sample.
* @param fileData Content of the input file.
* @param instrumentation To be used to load the sample file.
* @throws IOException If reading from the input fails.
* @throws InterruptedException If interrupted while reading from the input.
* @see #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, boolean)
*/
public static void assertOutput(ExtractorFactory factory, String sampleFile, byte[] fileData,
Instrumentation instrumentation) throws IOException, InterruptedException {
assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, false, false);
assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, false, false);
assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, true, false);
assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, true, false);
assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, false, true);
assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, false, true);
assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, true, true);
assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, true, true);
}
/**
* Asserts that {@code extractor} consumes {@code sampleFile} successfully and its output equals
* to a prerecorded output dump file with the name {@code sampleFile} + "{@value
* #DUMP_EXTENSION}". If {@code simulateUnknownLength} is true and {@code sampleFile} + "{@value
* #UNKNOWN_LENGTH_EXTENSION}" exists, it's preferred.
*
* @param extractor The {@link Extractor} to be tested.
* @param sampleFile The path to the input sample.
* @param fileData Content of the input file.
* @param instrumentation To be used to load the sample file.
* @param simulateIOErrors If true simulates IOErrors.
* @param simulateUnknownLength If true simulates unknown input length.
* @param simulatePartialReads If true simulates partial reads.
* @return The {@link FakeExtractorOutput} used in the test.
* @throws IOException If reading from the input fails.
* @throws InterruptedException If interrupted while reading from the input.
*/
public static FakeExtractorOutput assertOutput(Extractor extractor, String sampleFile,
byte[] fileData, Instrumentation instrumentation, boolean simulateIOErrors,
boolean simulateUnknownLength, boolean simulatePartialReads) throws IOException,
InterruptedException {
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData)
.setSimulateIOErrors(simulateIOErrors)
.setSimulateUnknownLength(simulateUnknownLength)
.setSimulatePartialReads(simulatePartialReads).build();
Assert.assertTrue(sniffTestData(extractor, input));
input.resetPeekPosition();
FakeExtractorOutput extractorOutput = consumeTestData(extractor, input, 0, true);
if (simulateUnknownLength
&& assetExists(instrumentation, sampleFile + UNKNOWN_LENGTH_EXTENSION)) {
extractorOutput.assertOutput(instrumentation, sampleFile + UNKNOWN_LENGTH_EXTENSION);
} else {
extractorOutput.assertOutput(instrumentation, sampleFile + ".0" + DUMP_EXTENSION);
}
SeekMap seekMap = extractorOutput.seekMap;
if (seekMap.isSeekable()) {
long durationUs = seekMap.getDurationUs();
for (int j = 0; j < 4; j++) {
long timeUs = (durationUs * j) / 3;
long position = seekMap.getPosition(timeUs);
input.setPosition((int) position);
for (int i = 0; i < extractorOutput.numberOfTracks; i++) {
extractorOutput.trackOutputs.valueAt(i).clear();
}
consumeTestData(extractor, input, timeUs, extractorOutput, false);
extractorOutput.assertOutput(instrumentation, sampleFile + '.' + j + DUMP_EXTENSION);
}
}
return extractorOutput;
}
/**
* Calls {@link #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean)} with all
* possible combinations of "simulate" parameters.
*
* @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor}
* class which is to be tested.
* @param sampleFile The path to the input sample.
* @param instrumentation To be used to load the sample file.
* @param expectedThrowable Expected {@link Throwable} class.
* @throws IOException If reading from the input fails.
* @throws InterruptedException If interrupted while reading from the input.
* @see #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean)
*/
public static void assertThrows(ExtractorFactory factory, String sampleFile,
Instrumentation instrumentation, Class<? extends Throwable> expectedThrowable)
throws IOException, InterruptedException {
byte[] fileData = getByteArray(instrumentation, sampleFile);
assertThrows(factory, fileData, expectedThrowable);
}
/**
* Calls {@link #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean)} with all
* possible combinations of "simulate" parameters.
*
* @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor}
* class which is to be tested.
* @param fileData Content of the input file.
* @param expectedThrowable Expected {@link Throwable} class.
* @throws IOException If reading from the input fails.
* @throws InterruptedException If interrupted while reading from the input.
* @see #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean)
*/
public static void assertThrows(ExtractorFactory factory, byte[] fileData,
Class<? extends Throwable> expectedThrowable) throws IOException, InterruptedException {
assertThrows(factory.create(), fileData, expectedThrowable, false, false, false);
assertThrows(factory.create(), fileData, expectedThrowable, true, false, false);
assertThrows(factory.create(), fileData, expectedThrowable, false, true, false);
assertThrows(factory.create(), fileData, expectedThrowable, true, true, false);
assertThrows(factory.create(), fileData, expectedThrowable, false, false, true);
assertThrows(factory.create(), fileData, expectedThrowable, true, false, true);
assertThrows(factory.create(), fileData, expectedThrowable, false, true, true);
assertThrows(factory.create(), fileData, expectedThrowable, true, true, true);
}
/**
* Asserts {@code extractor} throws {@code expectedThrowable} while consuming {@code sampleFile}.
*
* @param extractor The {@link Extractor} to be tested.
* @param fileData Content of the input file.
* @param expectedThrowable Expected {@link Throwable} class.
* @param simulateIOErrors If true simulates IOErrors.
* @param simulateUnknownLength If true simulates unknown input length.
* @param simulatePartialReads If true simulates partial reads.
* @throws IOException If reading from the input fails.
* @throws InterruptedException If interrupted while reading from the input.
*/
public static void assertThrows(Extractor extractor, byte[] fileData,
Class<? extends Throwable> expectedThrowable, boolean simulateIOErrors,
boolean simulateUnknownLength, boolean simulatePartialReads) throws IOException,
InterruptedException {
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData)
.setSimulateIOErrors(simulateIOErrors)
.setSimulateUnknownLength(simulateUnknownLength)
.setSimulatePartialReads(simulatePartialReads).build();
try {
consumeTestData(extractor, input, 0, true);
throw new AssertionError(expectedThrowable.getSimpleName() + " expected but not thrown");
} catch (Throwable throwable) {
if (expectedThrowable.equals(throwable.getClass())) {
return; // Pass!
}
throw throwable;
}
}
}