CallbackHelper.java revision 5d1f7b1de12d16ceb2c938c56701a3e8bfa558f7
1// Copyright 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.content.browser.test.util;
6
7import static org.chromium.base.test.util.ScalableTimeout.scaleTimeout;
8
9import java.util.concurrent.TimeUnit;
10import java.util.concurrent.TimeoutException;
11
12/**
13 * A helper class that encapsulates listening and blocking for callbacks.
14 *
15 * Sample usage:
16 *
17 * // Let us assume that this interface is defined by some piece of production code and is used
18 * // to communicate events that occur in that piece of code. Let us further assume that the
19 * // production code runs on the main thread test code runs on a separate test thread.
20 * // An instance that implements this interface would be injected by test code to ensure that the
21 * // methods are being called on another thread.
22 * interface Delegate {
23 *     void onOperationFailed(String errorMessage);
24 *     void onDataPersisted();
25 * }
26 *
27 * // This is the inner class you'd write in your test case to later inject into the production
28 * // code.
29 * class TestDelegate implements Delegate {
30 *     // This is the preferred way to create a helper that stores the parameters it receives
31 *     // when called by production code.
32 *     public static class OnOperationFailedHelper extends CallbackHelper {
33 *         private String mErrorMessage;
34 *
35 *         public void getErrorMessage() {
36 *             assert getCallCount() > 0;
37 *             return mErrorMessage;
38 *         }
39 *
40 *         public void notifyCalled(String errorMessage) {
41 *             mErrorMessage = errorMessage;
42 *             // It's important to call this after all parameter assignments.
43 *             notifyCalled();
44 *         }
45 *     }
46 *
47 *     // There should be one CallbackHelper instance per method.
48 *     private OnOperationFailedHelper mOnOperationFailedHelper;
49 *     private CallbackHelper mOnDataPersistedHelper;
50 *
51 *     public OnOperationFailedHelper getOnOperationFailedHelper() {
52 *         return mOnOperationFailedHelper;
53 *     }
54 *
55 *     public CallbackHelper getOnDataPersistedHelper() {
56 *         return mOnDataPersistedHelper;
57 *     }
58 *
59 *     @Override
60 *     public void onOperationFailed(String errorMessage) {
61 *         mOnOperationFailedHelper.notifyCalled(errorMessage);
62 *     }
63 *
64 *     @Override
65 *     public void onDataPersisted() {
66 *         mOnDataPersistedHelper.notifyCalled();
67 *     }
68 * }
69 *
70 * // This is a sample test case.
71 * public void testCase() throws Exception {
72 *     // Create the TestDelegate to inject into production code.
73 *     TestDelegate delegate = new TestDelegate();
74 *     // Create the production class instance that is being tested and inject the test delegate.
75 *     CodeUnderTest codeUnderTest = new CodeUnderTest();
76 *     codeUnderTest.setDelegate(delegate);
77 *
78 *     // Typically you'd get the current call count before performing the operation you expect to
79 *     // trigger the callback. There can't be any callbacks 'in flight' at this moment, otherwise
80 *     // the call count is unpredictable and the test will be flaky.
81 *     int onOperationFailedCallCount = delegate.getOnOperationFailedHelper().getCallCount();
82 *     codeUnderTest.doSomethingThatEndsUpCallingOnOperationFailedFromAnotherThread();
83 *     // It's safe to do other stuff here, if needed.
84 *     ....
85 *     // Wait for the callback if it hadn't been called yet, otherwise return immediately. This
86 *     // can throw an exception if the callback doesn't arrive within the timeout.
87 *     delegate.getOnOperationFailedHelper().waitForCallback(onOperationFailedCallCount);
88 *     // Access to method parameters is now safe.
89 *     assertEquals("server error", delegate.getOnOperationFailedHelper().getErrorMessage());
90 *
91 *     // Being able to pass the helper around lets us build methods which encapsulate commonly
92 *     // performed tasks.
93 *     doSomeOperationAndWait(codeUnerTest, delegate.getOnOperationFailedHelper());
94 *
95 *     // The helper can be resued for as many calls as needed, just be sure to get the count each
96 *     // time.
97 *     onOperationFailedCallCount = delegate.getOnOperationFailedHelper().getCallCount();
98 *     codeUnderTest.doSomethingElseButStillFailOnAnotherThread();
99 *     delegate.getOnOperationFailedHelper().waitForCallback(onOperationFailedCallCount);
100 *
101 *     // It is also possible to use more than one helper at a time.
102 *     onOperationFailedCallCount = delegate.getOnOperationFailedHelper().getCallCount();
103 *     int onDataPersistedCallCount = delegate.getOnDataPersistedHelper().getCallCount();
104 *     codeUnderTest.doSomethingThatPersistsDataButFailsInSomeOtherWayOnAnotherThread();
105 *     delegate.getOnDataPersistedHelper().waitForCallback(onDataPersistedCallCount);
106 *     delegate.getOnOperationFailedHelper().waitForCallback(onOperationFailedCallCount);
107 * }
108 *
109 * // Shows how to turn an async operation + completion callback into a synchronous operation.
110 * private void doSomeOperationAndWait(final CodeUnderTest underTest,
111 *         CallbackHelper operationHelper) throws InterruptedException, TimeoutException {
112 *     final int callCount = operaitonHelper.getCallCount();
113 *     getInstrumentaiton().runOnMainSync(new Runnable() {
114 *         @Override
115 *         public void run() {
116 *             // This schedules a call to a method on the injected TestDelegate. The TestDelegate
117 *             // implementation will then call operationHelper.notifyCalled().
118 *             underTest.operation();
119 *         }
120 *      });
121 *      operationHelper.waitForCallback(callCount);
122 * }
123 *
124 */
125public class CallbackHelper {
126    protected static final long WAIT_TIMEOUT_SECONDS = scaleTimeout(5);
127
128    private final Object mLock = new Object();
129    private int mCallCount = 0;
130
131    /**
132     * Gets the number of times the callback has been called.
133     *
134     * The call count can be used with the waitForCallback() method, indicating a point
135     * in time after which the caller wishes to record calls to the callback.
136     *
137     * In order to wait for a callback caused by X, the call count should be obtained
138     * before X occurs.
139     *
140     * NOTE: any call to the callback that occurs after the call count is obtained
141     * will result in the corresponding wait call to resume execution. The call count
142     * is intended to 'catch' callbacks that occur after X but before waitForCallback()
143     * is called.
144     */
145    public int getCallCount() {
146        synchronized (mLock) {
147            return mCallCount;
148        }
149    }
150
151    /**
152     * Blocks until the callback is called the specified number of
153     * times or throws an exception if we exceeded the specified time frame.
154     *
155     * This will wait for a callback to be called a specified number of times after
156     * the point in time at which the call count was obtained.  The method will return
157     * immediately if a call occurred the specified number of times after the
158     * call count was obtained but before the method was called, otherwise the method will
159     * block until the specified call count is reached.
160     *
161     * @param currentCallCount the value obtained by calling getCallCount().
162     * @param numberOfCallsToWaitFor number of calls (counting since
163     *                               currentCallCount was obtained) that we will wait for.
164     * @param timeout timeout value. We will wait the specified amount of time for a single
165     *                callback to occur so the method call may block up to
166     *                <code>numberOfCallsToWaitFor * timeout</code> units.
167     * @param unit timeout unit.
168     * @throws InterruptedException
169     * @throws TimeoutException Thrown if the method times out before onPageFinished is called.
170     */
171    public void waitForCallback(int currentCallCount, int numberOfCallsToWaitFor, long timeout,
172            TimeUnit unit) throws InterruptedException, TimeoutException {
173        assert mCallCount >= currentCallCount;
174        assert numberOfCallsToWaitFor > 0;
175        synchronized (mLock) {
176            int callCountWhenDoneWaiting = currentCallCount + numberOfCallsToWaitFor;
177            while (callCountWhenDoneWaiting > mCallCount) {
178                int callCountBeforeWait = mCallCount;
179                mLock.wait(unit.toMillis(timeout));
180                if (callCountBeforeWait == mCallCount) {
181                    throw new TimeoutException("waitForCallback timed out!");
182                }
183            }
184        }
185    }
186
187    public void waitForCallback(int currentCallCount, int numberOfCallsToWaitFor)
188            throws InterruptedException, TimeoutException {
189        waitForCallback(currentCallCount, numberOfCallsToWaitFor,
190                WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
191    }
192
193    public void waitForCallback(int currentCallCount)
194            throws InterruptedException, TimeoutException {
195        waitForCallback(currentCallCount, 1);
196    }
197
198    /**
199     * Blocks until the criteria is satisfied or throws an exception
200     * if the specified time frame is exceeded.
201     * @param timeout timeout value.
202     * @param unit timeout unit.
203     */
204    public void waitUntilCriteria(Criteria criteria, long timeout, TimeUnit unit)
205            throws InterruptedException, TimeoutException {
206        synchronized (mLock) {
207            final long startTime = System.currentTimeMillis();
208            boolean isSatisfied = criteria.isSatisfied();
209            while (!isSatisfied &&
210                    System.currentTimeMillis() - startTime < unit.toMillis(timeout)) {
211                mLock.wait(unit.toMillis(timeout));
212                isSatisfied = criteria.isSatisfied();
213            }
214            if (!isSatisfied) throw new TimeoutException("waitUntilCriteria timed out!");
215        }
216    }
217
218    public void waitUntilCriteria(Criteria criteria)
219            throws InterruptedException, TimeoutException {
220        waitUntilCriteria(criteria, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
221    }
222
223    /**
224     * Should be called when the callback associated with this helper object is called.
225     */
226    public void notifyCalled() {
227        synchronized (mLock) {
228            mCallCount++;
229            mLock.notifyAll();
230        }
231    }
232}
233