blob: 38f8926acd0e12f48071f096de4b2c68a3d3308e [file] [log] [blame]
// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.net;
import android.os.ConditionVariable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.util.ArrayList;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
/** An UploadDataProvider implementation used in tests. */
public class TestUploadDataProvider extends UploadDataProvider {
// Indicates whether all success callbacks are synchronous or asynchronous.
// Doesn't apply to errors.
public enum SuccessCallbackMode {
SYNC,
ASYNC
}
// Indicates whether failures should throw exceptions, invoke callbacks
// synchronously, or invoke callback asynchronously.
public enum FailMode {
NONE,
THROWN,
CALLBACK_SYNC,
CALLBACK_ASYNC
}
private ArrayList<byte[]> mReads = new ArrayList<byte[]>();
private final SuccessCallbackMode mSuccessCallbackMode;
private final Executor mExecutor;
private boolean mChunked;
// Index of read to fail on.
private int mReadFailIndex = -1;
// Indicates how to fail on a read.
private FailMode mReadFailMode = FailMode.NONE;
private FailMode mRewindFailMode = FailMode.NONE;
private FailMode mLengthFailMode = FailMode.NONE;
private int mNumReadCalls;
private int mNumRewindCalls;
private int mNextRead;
private boolean mStarted;
private boolean mReadPending;
private boolean mRewindPending;
// Used to ensure there are no read/rewind requests after a failure.
private boolean mFailed;
private AtomicBoolean mClosed = new AtomicBoolean(false);
private ConditionVariable mAwaitingClose = new ConditionVariable(false);
public TestUploadDataProvider(
SuccessCallbackMode successCallbackMode, final Executor executor) {
mSuccessCallbackMode = successCallbackMode;
mExecutor = executor;
}
// Adds the result to be returned by a successful read request. The
// returned bytes must all fit within the read buffer provided by Cronet.
// After a rewind, if there is one, all reads will be repeated.
public void addRead(byte[] read) {
if (mStarted) {
throw new IllegalStateException("Adding bytes after read");
}
mReads.add(read);
}
public void setReadFailure(int readFailIndex, FailMode readFailMode) {
mReadFailIndex = readFailIndex;
mReadFailMode = readFailMode;
}
public void setLengthFailure() {
mLengthFailMode = FailMode.THROWN;
}
public void setRewindFailure(FailMode rewindFailMode) {
mRewindFailMode = rewindFailMode;
}
public void setChunked(boolean chunked) {
mChunked = chunked;
}
public int getNumReadCalls() {
return mNumReadCalls;
}
public int getNumRewindCalls() {
return mNumRewindCalls;
}
/** Returns the cumulative length of all data added by calls to addRead. */
@Override
public long getLength() throws IOException {
if (mClosed.get()) {
throw new ClosedChannelException();
}
if (mLengthFailMode == FailMode.THROWN) {
throw new IllegalStateException("Sync length failure");
}
return getUploadedLength();
}
public long getUploadedLength() {
if (mChunked) {
return -1;
}
long length = 0;
for (byte[] read : mReads) {
length += read.length;
}
return length;
}
@Override
public void read(final UploadDataSink uploadDataSink, final ByteBuffer byteBuffer)
throws IOException {
int currentReadCall = mNumReadCalls;
++mNumReadCalls;
if (mClosed.get()) {
throw new ClosedChannelException();
}
assertIdle();
if (maybeFailRead(currentReadCall, uploadDataSink)) {
mFailed = true;
return;
}
mReadPending = true;
mStarted = true;
final boolean finalChunk = (mChunked && mNextRead == mReads.size() - 1);
if (mNextRead < mReads.size()) {
if ((byteBuffer.limit() - byteBuffer.position()) < mReads.get(mNextRead).length) {
throw new IllegalStateException("Read buffer smaller than expected.");
}
byteBuffer.put(mReads.get(mNextRead));
++mNextRead;
} else {
throw new IllegalStateException("Too many reads: " + mNextRead);
}
Runnable completeRunnable =
new Runnable() {
@Override
public void run() {
mReadPending = false;
uploadDataSink.onReadSucceeded(finalChunk);
}
};
if (mSuccessCallbackMode == SuccessCallbackMode.SYNC) {
completeRunnable.run();
} else {
mExecutor.execute(completeRunnable);
}
}
@Override
public void rewind(final UploadDataSink uploadDataSink) throws IOException {
++mNumRewindCalls;
if (mClosed.get()) {
throw new ClosedChannelException();
}
assertIdle();
if (maybeFailRewind(uploadDataSink)) {
mFailed = true;
return;
}
if (mNextRead == 0) {
// Should never try and rewind when rewinding does nothing.
throw new IllegalStateException("Unexpected rewind when already at beginning");
}
mRewindPending = true;
mNextRead = 0;
Runnable completeRunnable =
new Runnable() {
@Override
public void run() {
mRewindPending = false;
uploadDataSink.onRewindSucceeded();
}
};
if (mSuccessCallbackMode == SuccessCallbackMode.SYNC) {
completeRunnable.run();
} else {
mExecutor.execute(completeRunnable);
}
}
private void assertIdle() {
if (mReadPending) {
throw new IllegalStateException("Unexpected operation during read");
}
if (mRewindPending) {
throw new IllegalStateException("Unexpected operation during rewind");
}
if (mFailed) {
throw new IllegalStateException("Unexpected operation after failure");
}
}
private boolean maybeFailRead(int readIndex, final UploadDataSink uploadDataSink) {
if (readIndex != mReadFailIndex) return false;
switch (mReadFailMode) {
case THROWN:
throw new IllegalStateException("Thrown read failure");
case CALLBACK_SYNC:
uploadDataSink.onReadError(new IllegalStateException("Sync read failure"));
return true;
case CALLBACK_ASYNC:
Runnable errorRunnable =
new Runnable() {
@Override
public void run() {
uploadDataSink.onReadError(
new IllegalStateException("Async read failure"));
}
};
mExecutor.execute(errorRunnable);
return true;
default:
return false;
}
}
private boolean maybeFailRewind(final UploadDataSink uploadDataSink) {
switch (mRewindFailMode) {
case THROWN:
throw new IllegalStateException("Thrown rewind failure");
case CALLBACK_SYNC:
uploadDataSink.onRewindError(new IllegalStateException("Sync rewind failure"));
return true;
case CALLBACK_ASYNC:
Runnable errorRunnable =
new Runnable() {
@Override
public void run() {
uploadDataSink.onRewindError(
new IllegalStateException("Async rewind failure"));
}
};
mExecutor.execute(errorRunnable);
return true;
default:
return false;
}
}
@Override
public void close() throws IOException {
if (!mClosed.compareAndSet(false, true)) {
throw new AssertionError("Closed twice");
}
mAwaitingClose.open();
}
public void assertClosed() {
mAwaitingClose.block(5000);
if (!mClosed.get()) {
throw new AssertionError("Was not closed");
}
}
}