• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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