CallbackHelper.java revision a1401311d1ab56c4ed0a474bd38c108f75cb0cd9
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 android.os.SystemClock;
10
11import java.util.concurrent.TimeUnit;
12import 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 resued 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 = operaitonHelper.getCallCount();
115 *     getInstrumentaiton().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 */
127public class CallbackHelper {
128    protected static final long WAIT_TIMEOUT_SECONDS = scaleTimeout(5);
129
130    private final Object mLock = new Object();
131    private int mCallCount = 0;
132
133    /**
134     * Gets the number of times the callback has been called.
135     *
136     * The call count can be used with the waitForCallback() method, indicating a point
137     * in time after which the caller wishes to record calls to the callback.
138     *
139     * In order to wait for a callback caused by X, the call count should be obtained
140     * before X occurs.
141     *
142     * NOTE: any call to the callback that occurs after the call count is obtained
143     * will result in the corresponding wait call to resume execution. The call count
144     * is intended to 'catch' callbacks that occur after X but before waitForCallback()
145     * is called.
146     */
147    public int getCallCount() {
148        synchronized (mLock) {
149            return mCallCount;
150        }
151    }
152
153    /**
154     * Blocks until the callback is called the specified number of
155     * times or throws an exception if we exceeded the specified time frame.
156     *
157     * This will wait for a callback to be called a specified number of times after
158     * the point in time at which the call count was obtained.  The method will return
159     * immediately if a call occurred the specified number of times after the
160     * call count was obtained but before the method was called, otherwise the method will
161     * block until the specified call count is reached.
162     *
163     * @param currentCallCount the value obtained by calling getCallCount().
164     * @param numberOfCallsToWaitFor number of calls (counting since
165     *                               currentCallCount was obtained) that we will wait for.
166     * @param timeout timeout value. We will wait the specified amount of time for a single
167     *                callback to occur so the method call may block up to
168     *                <code>numberOfCallsToWaitFor * timeout</code> units.
169     * @param unit timeout unit.
170     * @throws InterruptedException
171     * @throws TimeoutException Thrown if the method times out before onPageFinished is called.
172     */
173    public void waitForCallback(int currentCallCount, int numberOfCallsToWaitFor, long timeout,
174            TimeUnit unit) throws InterruptedException, TimeoutException {
175        assert mCallCount >= currentCallCount;
176        assert numberOfCallsToWaitFor > 0;
177        synchronized (mLock) {
178            int callCountWhenDoneWaiting = currentCallCount + numberOfCallsToWaitFor;
179            while (callCountWhenDoneWaiting > mCallCount) {
180                int callCountBeforeWait = mCallCount;
181                mLock.wait(unit.toMillis(timeout));
182                if (callCountBeforeWait == mCallCount) {
183                    throw new TimeoutException("waitForCallback timed out!");
184                }
185            }
186        }
187    }
188
189    public void waitForCallback(int currentCallCount, int numberOfCallsToWaitFor)
190            throws InterruptedException, TimeoutException {
191        waitForCallback(currentCallCount, numberOfCallsToWaitFor,
192                WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
193    }
194
195    public void waitForCallback(int currentCallCount)
196            throws InterruptedException, TimeoutException {
197        waitForCallback(currentCallCount, 1);
198    }
199
200    /**
201     * Blocks until the criteria is satisfied or throws an exception
202     * if the specified time frame is exceeded.
203     * @param timeout timeout value.
204     * @param unit timeout unit.
205     */
206    public void waitUntilCriteria(Criteria criteria, long timeout, TimeUnit unit)
207            throws InterruptedException, TimeoutException {
208        synchronized (mLock) {
209            final long startTime = SystemClock.uptimeMillis();
210            boolean isSatisfied = criteria.isSatisfied();
211            while (!isSatisfied &&
212                    SystemClock.uptimeMillis() - startTime < unit.toMillis(timeout)) {
213                mLock.wait(unit.toMillis(timeout));
214                isSatisfied = criteria.isSatisfied();
215            }
216            if (!isSatisfied) throw new TimeoutException("waitUntilCriteria timed out!");
217        }
218    }
219
220    public void waitUntilCriteria(Criteria criteria)
221            throws InterruptedException, TimeoutException {
222        waitUntilCriteria(criteria, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
223    }
224
225    /**
226     * Should be called when the callback associated with this helper object is called.
227     */
228    public void notifyCalled() {
229        synchronized (mLock) {
230            mCallCount++;
231            mLock.notifyAll();
232        }
233    }
234}
235