• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2012 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.base.test.util;
6 
7 import org.hamcrest.MatcherAssert;
8 import org.hamcrest.Matchers;
9 import org.junit.Assert;
10 
11 import java.util.concurrent.TimeUnit;
12 import java.util.concurrent.TimeoutException;
13 
14 /**
15  * A helper class that encapsulates listening and blocking for callbacks.
16  *
17  * Sample usage:
18  *
19  * // Let us assume that this interface is defined by some piece of production code and is used
20  * // to communicate events that occur in that piece of code. Let us further assume that the
21  * // production code runs on the main thread test code runs on a separate test thread.
22  * // An instance that implements this interface would be injected by test code to ensure that the
23  * // methods are being called on another thread.
24  * interface Delegate {
25  *     void onOperationFailed(String errorMessage);
26  *     void onDataPersisted();
27  * }
28  *
29  * // This is the inner class you'd write in your test case to later inject into the production
30  * // code.
31  * class TestDelegate implements Delegate {
32  *     // This is the preferred way to create a helper that stores the parameters it receives
33  *     // when called by production code.
34  *     public static class OnOperationFailedHelper extends CallbackHelper {
35  *         private String mErrorMessage;
36  *
37  *         public void getErrorMessage() {
38  *             assert getCallCount() > 0;
39  *             return mErrorMessage;
40  *         }
41  *
42  *         public void notifyCalled(String errorMessage) {
43  *             mErrorMessage = errorMessage;
44  *             // It's important to call this after all parameter assignments.
45  *             notifyCalled();
46  *         }
47  *     }
48  *
49  *     // There should be one CallbackHelper instance per method.
50  *     private OnOperationFailedHelper mOnOperationFailedHelper;
51  *     private CallbackHelper mOnDataPersistedHelper;
52  *
53  *     public OnOperationFailedHelper getOnOperationFailedHelper() {
54  *         return mOnOperationFailedHelper;
55  *     }
56  *
57  *     public CallbackHelper getOnDataPersistedHelper() {
58  *         return mOnDataPersistedHelper;
59  *     }
60  *
61  *     @Override
62  *     public void onOperationFailed(String errorMessage) {
63  *         mOnOperationFailedHelper.notifyCalled(errorMessage);
64  *     }
65  *
66  *     @Override
67  *     public void onDataPersisted() {
68  *         mOnDataPersistedHelper.notifyCalled();
69  *     }
70  * }
71  *
72  * // This is a sample test case.
73  * public void testCase() throws Exception {
74  *     // Create the TestDelegate to inject into production code.
75  *     TestDelegate delegate = new TestDelegate();
76  *     // Create the production class instance that is being tested and inject the test delegate.
77  *     CodeUnderTest codeUnderTest = new CodeUnderTest();
78  *     codeUnderTest.setDelegate(delegate);
79  *
80  *     // Typically you'd get the current call count before performing the operation you expect to
81  *     // trigger the callback. There can't be any callbacks 'in flight' at this moment, otherwise
82  *     // the call count is unpredictable and the test will be flaky.
83  *     int onOperationFailedCallCount = delegate.getOnOperationFailedHelper().getCallCount();
84  *     codeUnderTest.doSomethingThatEndsUpCallingOnOperationFailedFromAnotherThread();
85  *     // It's safe to do other stuff here, if needed.
86  *     ....
87  *     // Wait for the callback if it hadn't been called yet, otherwise return immediately. This
88  *     // can throw an exception if the callback doesn't arrive within the timeout.
89  *     delegate.getOnOperationFailedHelper().waitForCallback(onOperationFailedCallCount);
90  *     // Access to method parameters is now safe.
91  *     assertEquals("server error", delegate.getOnOperationFailedHelper().getErrorMessage());
92  *
93  *     // Being able to pass the helper around lets us build methods which encapsulate commonly
94  *     // performed tasks.
95  *     doSomeOperationAndWait(codeUnerTest, delegate.getOnOperationFailedHelper());
96  *
97  *     // The helper can be reused for as many calls as needed, just be sure to get the count each
98  *     // time.
99  *     onOperationFailedCallCount = delegate.getOnOperationFailedHelper().getCallCount();
100  *     codeUnderTest.doSomethingElseButStillFailOnAnotherThread();
101  *     delegate.getOnOperationFailedHelper().waitForCallback(onOperationFailedCallCount);
102  *
103  *     // It is also possible to use more than one helper at a time.
104  *     onOperationFailedCallCount = delegate.getOnOperationFailedHelper().getCallCount();
105  *     int onDataPersistedCallCount = delegate.getOnDataPersistedHelper().getCallCount();
106  *     codeUnderTest.doSomethingThatPersistsDataButFailsInSomeOtherWayOnAnotherThread();
107  *     delegate.getOnDataPersistedHelper().waitForCallback(onDataPersistedCallCount);
108  *     delegate.getOnOperationFailedHelper().waitForCallback(onOperationFailedCallCount);
109  * }
110  *
111  * // Shows how to turn an async operation + completion callback into a synchronous operation.
112  * private void doSomeOperationAndWait(final CodeUnderTest underTest,
113  *         CallbackHelper operationHelper) throws InterruptedException, TimeoutException {
114  *     final int callCount = operationHelper.getCallCount();
115  *     getInstrumentation().runOnMainSync(new Runnable() {
116  *         @Override
117  *         public void run() {
118  *             // This schedules a call to a method on the injected TestDelegate. The TestDelegate
119  *             // implementation will then call operationHelper.notifyCalled().
120  *             underTest.operation();
121  *         }
122  *      });
123  *      operationHelper.waitForCallback(callCount);
124  * }
125  *
126  */
127 public class CallbackHelper {
128     /** The default timeout (in seconds) for a callback to wait. */
129     public static final long WAIT_TIMEOUT_SECONDS = 5L;
130 
131     private final Object mLock = new Object();
132     private int mCallCount;
133     private int mLastWaitedForCount;
134     private String mFailureString;
135     private boolean mSingleShotMode;
136 
137     /**
138      * Gets the number of times the callback has been called.
139      *
140      * The call count can be used with the waitForCallback() method, indicating a point
141      * in time after which the caller wishes to record calls to the callback.
142      *
143      * In order to wait for a callback caused by X, the call count should be obtained
144      * before X occurs.
145      *
146      * NOTE: any call to the callback that occurs after the call count is obtained
147      * will result in the corresponding wait call to resume execution. The call count
148      * is intended to 'catch' callbacks that occur after X but before waitForCallback()
149      * is called.
150      */
getCallCount()151     public int getCallCount() {
152         synchronized (mLock) {
153             return mCallCount;
154         }
155     }
156 
157     /**
158      * Blocks until the callback is called the specified number of times or throws an exception if
159      * we exceeded the specified time frame.
160      *
161      * <p>This will wait for a callback to be called a specified number of times after the point in
162      * time at which the call count was obtained. The method will return immediately if a call
163      * occurred the specified number of times after the call count was obtained but before the
164      * method was called, otherwise the method will block until the specified call count is reached.
165      *
166      * @param msg The error message to use if the callback times out.
167      * @param currentCallCount Wait until |notifyCalled| has been called this many times in total.
168      * @param numberOfCallsToWaitFor number of calls (counting since currentCallCount was obtained)
169      *     that we will wait for.
170      * @param timeout timeout value for all callbacks to occur.
171      * @param unit timeout unit.
172      * @throws TimeoutException Thrown if the method times out before onPageFinished is called.
173      */
waitForCallback( String msg, int currentCallCount, int numberOfCallsToWaitFor, long timeout, TimeUnit unit)174     public void waitForCallback(
175             String msg,
176             int currentCallCount,
177             int numberOfCallsToWaitFor,
178             long timeout,
179             TimeUnit unit)
180             throws TimeoutException {
181         assert mCallCount >= currentCallCount;
182         assert numberOfCallsToWaitFor > 0;
183         TimeoutTimer timer = new TimeoutTimer(unit.toMillis(timeout));
184         synchronized (mLock) {
185             int callCountWhenDoneWaiting = currentCallCount + numberOfCallsToWaitFor;
186             while (callCountWhenDoneWaiting > mCallCount && !timer.isTimedOut()) {
187                 try {
188                     mLock.wait(timer.getRemainingMs());
189                 } catch (InterruptedException e) {
190                     // Ignore the InterruptedException. Rely on the outer while loop to re-run.
191                 }
192                 if (mFailureString != null) {
193                     String s = mFailureString;
194                     mFailureString = null;
195                     Assert.fail(s);
196                 }
197             }
198             if (timer.isTimedOut()) {
199                 throw new TimeoutException(msg == null ? "waitForCallback timed out!" : msg);
200             }
201             mLastWaitedForCount = callCountWhenDoneWaiting;
202         }
203     }
204 
205     /**
206      * @see #waitForCallback(String, int, int, long, TimeUnit)
207      */
waitForCallback( int currentCallCount, int numberOfCallsToWaitFor, long timeout, TimeUnit unit)208     public void waitForCallback(
209             int currentCallCount, int numberOfCallsToWaitFor, long timeout, TimeUnit unit)
210             throws TimeoutException {
211         waitForCallback(null, currentCallCount, numberOfCallsToWaitFor, timeout, unit);
212     }
213 
214     /**
215      * @see #waitForCallback(String, int, int, long, TimeUnit)
216      */
waitForCallback(int currentCallCount, int numberOfCallsToWaitFor)217     public void waitForCallback(int currentCallCount, int numberOfCallsToWaitFor)
218             throws TimeoutException {
219         waitForCallback(
220                 null,
221                 currentCallCount,
222                 numberOfCallsToWaitFor,
223                 WAIT_TIMEOUT_SECONDS,
224                 TimeUnit.SECONDS);
225     }
226 
227     /**
228      * @see #waitForCallback(String, int, int, long, TimeUnit)
229      */
waitForCallback(String msg, int currentCallCount)230     public void waitForCallback(String msg, int currentCallCount) throws TimeoutException {
231         waitForCallback(msg, currentCallCount, 1, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
232     }
233 
234     /**
235      * @see #waitForCallback(String, int, int, long, TimeUnit)
236      */
waitForCallback(int currentCallCount)237     public void waitForCallback(int currentCallCount) throws TimeoutException {
238         waitForCallback(null, currentCallCount, 1, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
239     }
240 
241     /**
242      * Blocks until the next time the callback is called.
243      * @param msg The error message to use if the callback times out.
244      * @throws TimeoutException
245      */
waitForNext(String msg)246     public void waitForNext(String msg) throws TimeoutException {
247         waitForCallback(msg, mLastWaitedForCount, 1, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
248     }
249 
250     /** @see #waitForNext(String) */
waitForNext()251     public void waitForNext() throws TimeoutException {
252         waitForNext(null);
253     }
254 
255     /**
256      * Blocks until the next time the callback is called.
257      * @param timeout timeout value for all callbacks to occur.
258      * @param unit timeout unit.
259      * @throws TimeoutException
260      */
waitForNext(long timeout, TimeUnit unit)261     public void waitForNext(long timeout, TimeUnit unit) throws TimeoutException {
262         waitForCallback(null, mLastWaitedForCount, 1, timeout, unit);
263     }
264 
265     /** Wait until the callback has been called once. */
waitForFirst(String msg, long timeout, TimeUnit unit)266     public void waitForFirst(String msg, long timeout, TimeUnit unit) throws TimeoutException {
267         MatcherAssert.assertThat(
268                 "Use waitForCallback(currentCallCount) or waitForNext() for callbacks that are "
269                         + "called multiple times.",
270                 mCallCount,
271                 Matchers.lessThanOrEqualTo(1));
272         mSingleShotMode = true;
273         waitForCallback(msg, 0, 1, timeout, unit);
274     }
275 
276     /** Wait until the callback has been called once. */
waitForFirst(long timeout, TimeUnit unit)277     public void waitForFirst(long timeout, TimeUnit unit) throws TimeoutException {
278         waitForFirst(null, timeout, unit);
279     }
280 
281     /** Wait until the callback has been called once. */
waitForFirst(String msg)282     public void waitForFirst(String msg) throws TimeoutException {
283         waitForFirst(msg, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
284     }
285 
286     /** Wait until the callback has been called at least once. */
waitForFirst()287     public void waitForFirst() throws TimeoutException {
288         waitForFirst(null);
289     }
290 
291     /** Should be called when the callback associated with this helper object is called. */
notifyCalled()292     public void notifyCalled() {
293         notifyInternal(null);
294     }
295 
296     /**
297      * Should be called when the callback associated with this helper object wants to
298      * indicate a failure.
299      *
300      * @param s The failure message.
301      */
notifyFailed(String s)302     public void notifyFailed(String s) {
303         notifyInternal(s);
304     }
305 
notifyInternal(String failureString)306     private void notifyInternal(String failureString) {
307         synchronized (mLock) {
308             mCallCount++;
309             mFailureString = failureString;
310             if (mSingleShotMode && mCallCount > 1) {
311                 Assert.fail("Single-use callback called multiple times.");
312             }
313             mLock.notifyAll();
314         }
315     }
316 }
317