• 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 String mFailureString;
134     private boolean mSingleShotMode;
135 
136     /**
137      * Gets the number of times the callback has been called.
138      *
139      * The call count can be used with the waitForCallback() method, indicating a point
140      * in time after which the caller wishes to record calls to the callback.
141      *
142      * In order to wait for a callback caused by X, the call count should be obtained
143      * before X occurs.
144      *
145      * NOTE: any call to the callback that occurs after the call count is obtained
146      * will result in the corresponding wait call to resume execution. The call count
147      * is intended to 'catch' callbacks that occur after X but before waitForCallback()
148      * is called.
149      */
getCallCount()150     public int getCallCount() {
151         synchronized (mLock) {
152             return mCallCount;
153         }
154     }
155 
156     /**
157      * Blocks until the callback is called the specified number of
158      * times or throws an exception if we exceeded the specified time frame.
159      *
160      * This will wait for a callback to be called a specified number of times after
161      * the point in time at which the call count was obtained.  The method will return
162      * immediately if a call occurred the specified number of times after the
163      * call count was obtained but before the method was called, otherwise the method will
164      * 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
169      *                               currentCallCount was obtained) 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(String msg, int currentCallCount, int numberOfCallsToWaitFor,
175             long timeout, TimeUnit unit) throws TimeoutException {
176         assert mCallCount >= currentCallCount;
177         assert numberOfCallsToWaitFor > 0;
178         TimeoutTimer timer = new TimeoutTimer(unit.toMillis(timeout));
179         synchronized (mLock) {
180             int callCountWhenDoneWaiting = currentCallCount + numberOfCallsToWaitFor;
181             while (callCountWhenDoneWaiting > mCallCount && !timer.isTimedOut()) {
182                 try {
183                     mLock.wait(timer.getRemainingMs());
184                 } catch (InterruptedException e) {
185                     // Ignore the InterruptedException. Rely on the outer while loop to re-run.
186                 }
187                 if (mFailureString != null) {
188                     String s = mFailureString;
189                     mFailureString = null;
190                     Assert.fail(s);
191                 }
192             }
193             if (timer.isTimedOut()) {
194                 throw new TimeoutException(msg == null ? "waitForCallback timed out!" : msg);
195             }
196         }
197     }
198 
199     /**
200      * @see #waitForCallback(String, int, int, long, TimeUnit)
201      */
waitForCallback(int currentCallCount, int numberOfCallsToWaitFor, long timeout, TimeUnit unit)202     public void waitForCallback(int currentCallCount, int numberOfCallsToWaitFor, long timeout,
203             TimeUnit unit) throws TimeoutException {
204         waitForCallback(null, currentCallCount, numberOfCallsToWaitFor, timeout, unit);
205     }
206 
207     /**
208      * @see #waitForCallback(String, int, int, long, TimeUnit)
209      */
waitForCallback(int currentCallCount, int numberOfCallsToWaitFor)210     public void waitForCallback(int currentCallCount, int numberOfCallsToWaitFor)
211             throws TimeoutException {
212         waitForCallback(null, currentCallCount, numberOfCallsToWaitFor,
213                 WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
214     }
215 
216     /**
217      * @see #waitForCallback(String, int, int, long, TimeUnit)
218      */
waitForCallback(String msg, int currentCallCount)219     public void waitForCallback(String msg, int currentCallCount) throws TimeoutException {
220         waitForCallback(msg, currentCallCount, 1, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
221     }
222 
223     /**
224      * @see #waitForCallback(String, int, int, long, TimeUnit)
225      */
waitForCallback(int currentCallCount)226     public void waitForCallback(int currentCallCount) throws TimeoutException {
227         waitForCallback(null, currentCallCount, 1, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
228     }
229 
230     /**
231      * Blocks until the next time the callback is called.
232      * @param msg The error message to use if the callback times out.
233      * @throws TimeoutException
234      */
waitForNext(String msg)235     public void waitForNext(String msg) throws TimeoutException {
236         waitForCallback(msg, mCallCount, 1, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
237     }
238 
239     /** @see #waitForNext(String) */
waitForNext()240     public void waitForNext() throws TimeoutException {
241         waitForNext(null);
242     }
243 
244     /**
245      * Blocks until the next time the callback is called.
246      * @param timeout timeout value for all callbacks to occur.
247      * @param unit timeout unit.
248      * @throws TimeoutException
249      */
waitForNext(long timeout, TimeUnit unit)250     public void waitForNext(long timeout, TimeUnit unit) throws TimeoutException {
251         waitForCallback(null, mCallCount, 1, timeout, unit);
252     }
253 
254     /**
255      * Wait until the callback has been called once.
256      */
waitForFirst(String msg, long timeout, TimeUnit unit)257     public void waitForFirst(String msg, long timeout, TimeUnit unit) throws TimeoutException {
258         MatcherAssert.assertThat(
259                 "Use waitForCallback(currentCallCount) for callbacks that are called multiple "
260                         + "times.",
261                 mCallCount, Matchers.lessThanOrEqualTo(1));
262         mSingleShotMode = true;
263         waitForCallback(msg, 0, 1, timeout, unit);
264     }
265 
266     /**
267      * Wait until the callback has been called once.
268      */
waitForFirst(long timeout, TimeUnit unit)269     public void waitForFirst(long timeout, TimeUnit unit) throws TimeoutException {
270         waitForFirst(null, timeout, unit);
271     }
272 
273     /**
274      * Wait until the callback has been called once.
275      */
waitForFirst(String msg)276     public void waitForFirst(String msg) throws TimeoutException {
277         waitForFirst(msg, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
278     }
279 
280     /**
281      * Wait until the callback has been called at least once.
282      */
waitForFirst()283     public void waitForFirst() throws TimeoutException {
284         waitForFirst(null);
285     }
286 
287     /**
288      * Should be called when the callback associated with this helper object is called.
289      */
notifyCalled()290     public void notifyCalled() {
291         notifyInternal(null);
292     }
293 
294     /**
295      * Should be called when the callback associated with this helper object wants to
296      * indicate a failure.
297      *
298      * @param s The failure message.
299      */
notifyFailed(String s)300     public void notifyFailed(String s) {
301         notifyInternal(s);
302     }
303 
notifyInternal(String failureString)304     private void notifyInternal(String failureString) {
305         synchronized (mLock) {
306             mCallCount++;
307             mFailureString = failureString;
308             if (mSingleShotMode && mCallCount > 1) {
309                 Assert.fail("Single-use callback called multiple times.");
310             }
311             mLock.notifyAll();
312         }
313     }
314 }
315