1 // Copyright 2014 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.net; 6 7 import static com.google.common.truth.Truth.assertThat; 8 import static com.google.common.truth.Truth.assertWithMessage; 9 10 import static org.junit.Assert.fail; 11 12 import android.os.ConditionVariable; 13 import android.os.StrictMode; 14 15 import java.nio.ByteBuffer; 16 import java.util.ArrayList; 17 import java.util.concurrent.ExecutorService; 18 import java.util.concurrent.Executors; 19 import java.util.concurrent.ThreadFactory; 20 import java.util.concurrent.TimeUnit; 21 22 /** 23 * Callback that tracks information from different callbacks and and has a 24 * method to block thread until the request completes on another thread. 25 * Allows to cancel, block request or throw an exception from an arbitrary step. 26 */ 27 public class TestUrlRequestCallback extends UrlRequest.Callback { 28 public ArrayList<UrlResponseInfo> mRedirectResponseInfoList = new ArrayList<UrlResponseInfo>(); 29 public ArrayList<String> mRedirectUrlList = new ArrayList<String>(); 30 private UrlResponseInfo mResponseInfo; 31 public CronetException mError; 32 33 public ResponseStep mResponseStep = ResponseStep.NOTHING; 34 35 public int mRedirectCount; 36 public boolean mOnErrorCalled; 37 public boolean mOnCanceledCalled; 38 39 public int mHttpResponseDataLength; 40 public String mResponseAsString = ""; 41 42 public int mReadBufferSize = 32 * 1024; 43 44 // When false, the consumer is responsible for all calls into the request 45 // that advance it. 46 private boolean mAutoAdvance = true; 47 // Whether an exception is thrown by maybeThrowCancelOrPause(). 48 private boolean mCallbackExceptionThrown; 49 50 // Whether to permit calls on the network thread. 51 private boolean mAllowDirectExecutor; 52 53 // The executor thread will block on this after reaching a terminal method. 54 // Terminal methods are (onSucceeded, onFailed or onCancelled) 55 private ConditionVariable mBlockOnTerminalState = new ConditionVariable(true); 56 57 // Conditionally fail on certain steps. 58 private FailureType mFailureType = FailureType.NONE; 59 private ResponseStep mFailureStep = ResponseStep.NOTHING; 60 61 // Signals when request is done either successfully or not. 62 private final ConditionVariable mDone = new ConditionVariable(); 63 64 // Signaled on each step when mAutoAdvance is false. 65 private final ConditionVariable mStepBlock = new ConditionVariable(); 66 67 // Executor Service for Cronet callbacks. 68 private final ExecutorService mExecutorService; 69 private Thread mExecutorThread; 70 71 // position() of ByteBuffer prior to read() call. 72 private int mBufferPositionBeforeRead; 73 74 private static class ExecutorThreadFactory implements ThreadFactory { 75 @Override newThread(final Runnable r)76 public Thread newThread(final Runnable r) { 77 return new Thread( 78 new Runnable() { 79 @Override 80 public void run() { 81 StrictMode.ThreadPolicy threadPolicy = StrictMode.getThreadPolicy(); 82 try { 83 StrictMode.setThreadPolicy( 84 new StrictMode.ThreadPolicy.Builder() 85 .detectNetwork() 86 .penaltyLog() 87 .penaltyDeath() 88 .build()); 89 r.run(); 90 } finally { 91 StrictMode.setThreadPolicy(threadPolicy); 92 } 93 } 94 }); 95 } 96 } 97 98 public enum ResponseStep { 99 NOTHING, 100 ON_RECEIVED_REDIRECT, 101 ON_RESPONSE_STARTED, 102 ON_READ_COMPLETED, 103 ON_SUCCEEDED, 104 ON_FAILED, 105 ON_CANCELED, 106 } 107 108 public enum FailureType { 109 NONE, 110 CANCEL_SYNC, 111 CANCEL_ASYNC, 112 // Same as above, but continues to advance the request after posting 113 // the cancellation task. 114 CANCEL_ASYNC_WITHOUT_PAUSE, 115 THROW_SYNC 116 } 117 118 /** Set {@code mExecutorThread}. */ 119 private void fillInExecutorThread() { 120 mExecutorService.execute( 121 new Runnable() { 122 @Override 123 public void run() { 124 mExecutorThread = Thread.currentThread(); 125 } 126 }); 127 } 128 129 /** Create a {@link TestUrlRequestCallback} with a new single-threaded executor. */ 130 public TestUrlRequestCallback() { 131 this(Executors.newSingleThreadExecutor(new ExecutorThreadFactory())); 132 } 133 134 /** 135 * Create a {@link TestUrlRequestCallback} using a custom single-threaded executor. 136 * NOTE(pauljensen): {@code executorService} should be a new single-threaded executor. 137 */ 138 public TestUrlRequestCallback(ExecutorService executorService) { 139 mExecutorService = executorService; 140 fillInExecutorThread(); 141 } 142 143 /** 144 * This blocks the callback executor thread once it has reached a final state callback. 145 * In order to continue execution, this method must be called again and providing {@code false} 146 * to continue execution. 147 * @param blockOnTerminalState the state to set for the executor thread 148 */ 149 public void setBlockOnTerminalState(boolean blockOnTerminalState) { 150 if (blockOnTerminalState) { 151 mBlockOnTerminalState.close(); 152 } else { 153 mBlockOnTerminalState.open(); 154 } 155 } 156 157 public void setAutoAdvance(boolean autoAdvance) { 158 mAutoAdvance = autoAdvance; 159 } 160 161 public void setAllowDirectExecutor(boolean allowed) { 162 mAllowDirectExecutor = allowed; 163 } 164 165 public void setFailure(FailureType failureType, ResponseStep failureStep) { 166 mFailureStep = failureStep; 167 mFailureType = failureType; 168 } 169 170 public void blockForDone() { 171 mDone.block(); 172 } 173 174 public void blockForDone(long timeoutMs) { 175 assertWithMessage("Request didn't terminate in time").that(mDone.block(timeoutMs)).isTrue(); 176 } 177 178 public void waitForNextStep() { 179 mStepBlock.block(); 180 mStepBlock.close(); 181 } 182 183 public ExecutorService getExecutor() { 184 return mExecutorService; 185 } 186 187 public void shutdownExecutor() { 188 mExecutorService.shutdown(); 189 } 190 191 /** 192 * Shuts down the ExecutorService and waits until it executes all posted 193 * tasks. 194 */ 195 public void shutdownExecutorAndWait() { 196 mExecutorService.shutdown(); 197 try { 198 // Termination shouldn't take long. Use 1 min which should be more than enough. 199 mExecutorService.awaitTermination(1, TimeUnit.MINUTES); 200 } catch (InterruptedException e) { 201 fail("ExecutorService is interrupted while waiting for termination"); 202 } 203 assertThat(mExecutorService.isTerminated()).isTrue(); 204 } 205 206 @Override 207 public void onRedirectReceived( 208 UrlRequest request, UrlResponseInfo info, String newLocationUrl) { 209 checkExecutorThread(); 210 assertThat(request.isDone()).isFalse(); 211 assertThat(mResponseStep).isAnyOf(ResponseStep.NOTHING, ResponseStep.ON_RECEIVED_REDIRECT); 212 assertThat(mError).isNull(); 213 214 mResponseStep = ResponseStep.ON_RECEIVED_REDIRECT; 215 mRedirectUrlList.add(newLocationUrl); 216 mRedirectResponseInfoList.add(info); 217 ++mRedirectCount; 218 if (maybeThrowCancelOrPause(request)) { 219 return; 220 } 221 request.followRedirect(); 222 } 223 224 @Override 225 public void onResponseStarted(UrlRequest request, UrlResponseInfo info) { 226 checkExecutorThread(); 227 assertThat(request.isDone()).isFalse(); 228 assertThat(mResponseStep).isAnyOf(ResponseStep.NOTHING, ResponseStep.ON_RECEIVED_REDIRECT); 229 assertThat(mError).isNull(); 230 231 mResponseStep = ResponseStep.ON_RESPONSE_STARTED; 232 mResponseInfo = info; 233 if (maybeThrowCancelOrPause(request)) { 234 return; 235 } 236 startNextRead(request); 237 } 238 239 @Override 240 public void onReadCompleted(UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer) { 241 checkExecutorThread(); 242 assertThat(request.isDone()).isFalse(); 243 assertThat(mResponseStep) 244 .isAnyOf(ResponseStep.ON_RESPONSE_STARTED, ResponseStep.ON_READ_COMPLETED); 245 assertThat(mError).isNull(); 246 247 mResponseStep = ResponseStep.ON_READ_COMPLETED; 248 249 final byte[] lastDataReceivedAsBytes; 250 final int bytesRead = byteBuffer.position() - mBufferPositionBeforeRead; 251 mHttpResponseDataLength += bytesRead; 252 lastDataReceivedAsBytes = new byte[bytesRead]; 253 // Rewind |byteBuffer.position()| to pre-read() position. 254 byteBuffer.position(mBufferPositionBeforeRead); 255 // This restores |byteBuffer.position()| to its value on entrance to 256 // this function. 257 byteBuffer.get(lastDataReceivedAsBytes); 258 mResponseAsString += new String(lastDataReceivedAsBytes); 259 260 if (maybeThrowCancelOrPause(request)) { 261 return; 262 } 263 startNextRead(request); 264 } 265 266 @Override 267 public void onSucceeded(UrlRequest request, UrlResponseInfo info) { 268 checkExecutorThread(); 269 assertThat(request.isDone()).isTrue(); 270 assertThat(mResponseStep) 271 .isAnyOf(ResponseStep.ON_RESPONSE_STARTED, ResponseStep.ON_READ_COMPLETED); 272 assertThat(mOnErrorCalled).isFalse(); 273 assertThat(mOnCanceledCalled).isFalse(); 274 assertThat(mError).isNull(); 275 276 mResponseStep = ResponseStep.ON_SUCCEEDED; 277 mResponseInfo = info; 278 openDone(); 279 mBlockOnTerminalState.block(); 280 maybeThrowCancelOrPause(request); 281 } 282 283 @Override 284 public void onFailed(UrlRequest request, UrlResponseInfo info, CronetException error) { 285 // If the failure is because of prohibited direct execution, the test shouldn't fail 286 // since the request already did. 287 if (error.getCause() instanceof InlineExecutionProhibitedException) { 288 mAllowDirectExecutor = true; 289 } 290 checkExecutorThread(); 291 assertThat(request.isDone()).isTrue(); 292 // Shouldn't happen after success. 293 assertThat(mResponseStep).isNotEqualTo(ResponseStep.ON_SUCCEEDED); 294 // Should happen at most once for a single request. 295 assertThat(mError).isNull(); 296 assertThat(mOnErrorCalled).isFalse(); 297 assertThat(mOnCanceledCalled).isFalse(); 298 if (mCallbackExceptionThrown) { 299 assertThat(error).isInstanceOf(CallbackException.class); 300 assertThat(error) 301 .hasMessageThat() 302 .contains("Exception received from UrlRequest.Callback"); 303 assertThat(error).hasCauseThat().isInstanceOf(IllegalStateException.class); 304 assertThat(error).hasCauseThat().hasMessageThat().contains("Listener Exception."); 305 } 306 307 mResponseStep = ResponseStep.ON_FAILED; 308 mOnErrorCalled = true; 309 mError = error; 310 openDone(); 311 mBlockOnTerminalState.block(); 312 maybeThrowCancelOrPause(request); 313 } 314 315 @Override 316 public void onCanceled(UrlRequest request, UrlResponseInfo info) { 317 checkExecutorThread(); 318 assertThat(request.isDone()).isTrue(); 319 // Should happen at most once for a single request. 320 assertThat(mOnCanceledCalled).isFalse(); 321 assertThat(mOnErrorCalled).isFalse(); 322 assertThat(mError).isNull(); 323 324 mResponseStep = ResponseStep.ON_CANCELED; 325 mOnCanceledCalled = true; 326 openDone(); 327 mBlockOnTerminalState.block(); 328 maybeThrowCancelOrPause(request); 329 } 330 331 public void startNextRead(UrlRequest request) { 332 startNextRead(request, ByteBuffer.allocateDirect(mReadBufferSize)); 333 } 334 335 public void startNextRead(UrlRequest request, ByteBuffer buffer) { 336 mBufferPositionBeforeRead = buffer.position(); 337 request.read(buffer); 338 } 339 340 public boolean isDone() { 341 // It's not mentioned by the Android docs, but block(0) seems to block 342 // indefinitely, so have to block for one millisecond to get state 343 // without blocking. 344 return mDone.block(1); 345 } 346 347 /** 348 * Asserts that there is no callback error before trying to access responseInfo. Only use this 349 * when you expect {@code mError} to be null. 350 * @return {@link UrlResponseInfo} 351 */ 352 public UrlResponseInfo getResponseInfoWithChecks() { 353 assertThat(mError).isNull(); 354 assertThat(mOnErrorCalled).isFalse(); 355 assertThat(mResponseInfo).isNotNull(); 356 return mResponseInfo; 357 } 358 359 /** 360 * Simply returns {@code mResponseInfo} with no nullability or error checks. 361 * @return {@link UrlResponseInfo} 362 */ 363 public UrlResponseInfo getResponseInfo() { 364 return mResponseInfo; 365 } 366 367 protected void openDone() { 368 mDone.open(); 369 } 370 371 private void checkExecutorThread() { 372 if (!mAllowDirectExecutor) { 373 assertThat(Thread.currentThread()).isEqualTo(mExecutorThread); 374 } 375 } 376 377 /** 378 * Returns {@code false} if the listener should continue to advance the 379 * request. 380 */ 381 private boolean maybeThrowCancelOrPause(final UrlRequest request) { 382 checkExecutorThread(); 383 if (mResponseStep != mFailureStep || mFailureType == FailureType.NONE) { 384 if (!mAutoAdvance) { 385 mStepBlock.open(); 386 return true; 387 } 388 return false; 389 } 390 391 if (mFailureType == FailureType.THROW_SYNC) { 392 assertThat(mCallbackExceptionThrown).isFalse(); 393 mCallbackExceptionThrown = true; 394 throw new IllegalStateException("Listener Exception."); 395 } 396 Runnable task = 397 new Runnable() { 398 @Override 399 public void run() { 400 request.cancel(); 401 } 402 }; 403 if (mFailureType == FailureType.CANCEL_ASYNC 404 || mFailureType == FailureType.CANCEL_ASYNC_WITHOUT_PAUSE) { 405 getExecutor().execute(task); 406 } else { 407 task.run(); 408 } 409 return mFailureType != FailureType.CANCEL_ASYNC_WITHOUT_PAUSE; 410 } 411 } 412