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.
5 package org.chromium.content.browser.test.util;
7 import static org.chromium.base.test.util.ScalableTimeout.scaleTimeout;
9 import java.util.concurrent.TimeUnit;
10 import java.util.concurrent.TimeoutException;
13 * A helper class that encapsulates listening and blocking for callbacks.
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();
27 * // This is the inner class you'd write in your test case to later inject into the production
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;
35 * public void getErrorMessage() {
36 * assert getCallCount() > 0;
37 * return mErrorMessage;
40 * public void notifyCalled(String errorMessage) {
41 * mErrorMessage = errorMessage;
42 * // It's important to call this after all parameter assignments.
47 * // There should be one CallbackHelper instance per method.
48 * private OnOperationFailedHelper mOnOperationFailedHelper;
49 * private CallbackHelper mOnDataPersistedHelper;
51 * public OnOperationFailedHelper getOnOperationFailedHelper() {
52 * return mOnOperationFailedHelper;
55 * public CallbackHelper getOnDataPersistedHelper() {
56 * return mOnDataPersistedHelper;
60 * public void onOperationFailed(String errorMessage) {
61 * mOnOperationFailedHelper.notifyCalled(errorMessage);
65 * public void onDataPersisted() {
66 * mOnDataPersistedHelper.notifyCalled();
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);
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.
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());
91 * // Being able to pass the helper around lets us build methods which encapsulate commonly
93 * doSomeOperationAndWait(codeUnerTest, delegate.getOnOperationFailedHelper());
95 * // The helper can be resued for as many calls as needed, just be sure to get the count each
97 * onOperationFailedCallCount = delegate.getOnOperationFailedHelper().getCallCount();
98 * codeUnderTest.doSomethingElseButStillFailOnAnotherThread();
99 * delegate.getOnOperationFailedHelper().waitForCallback(onOperationFailedCallCount);
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);
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() {
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();
121 * operationHelper.waitForCallback(callCount);
125 public class CallbackHelper {
126 protected static final long WAIT_TIMEOUT_SECONDS = scaleTimeout(5);
128 private final Object mLock = new Object();
129 private int mCallCount = 0;
132 * Gets the number of times the callback has been called.
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.
137 * In order to wait for a callback caused by X, the call count should be obtained
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()
145 public int getCallCount() {
146 synchronized (mLock) {
152 * Blocks until the callback is called the specified number of
153 * times or throws an exception if we exceeded the specified time frame.
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.
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.
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!");
187 public void waitForCallback(int currentCallCount, int numberOfCallsToWaitFor)
188 throws InterruptedException, TimeoutException {
189 waitForCallback(currentCallCount, numberOfCallsToWaitFor,
190 WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
193 public void waitForCallback(int currentCallCount)
194 throws InterruptedException, TimeoutException {
195 waitForCallback(currentCallCount, 1);
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.
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();
214 if (!isSatisfied) throw new TimeoutException("waitUntilCriteria timed out!");
218 public void waitUntilCriteria(Criteria criteria)
219 throws InterruptedException, TimeoutException {
220 waitUntilCriteria(criteria, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
224 * Should be called when the callback associated with this helper object is called.
226 public void notifyCalled() {
227 synchronized (mLock) {