1 // Copyright (c) 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 java.util.concurrent.TimeUnit;
8 import java.util.concurrent.TimeoutException;
11 * A helper class that encapsulates listening and blocking for callbacks.
15 * // Let us assume that this interface is defined by some piece of production code and is used
16 * // to communicate events that occur in that piece of code. Let us further assume that the
17 * // production code runs on the main thread test code runs on a separate test thread.
18 * // An instance that implements this interface would be injected by test code to ensure that the
19 * // methods are being called on another thread.
20 * interface Delegate {
21 * void onOperationFailed(String errorMessage);
22 * void onDataPersisted();
25 * // This is the inner class you'd write in your test case to later inject into the production
27 * class TestDelegate implements Delegate {
28 * // This is the preferred way to create a helper that stores the parameters it receives
29 * // when called by production code.
30 * public static class OnOperationFailedHelper extends CallbackHelper {
31 * private String mErrorMessage;
33 * public void getErrorMessage() {
34 * assert getCallCount() > 0;
35 * return mErrorMessage;
38 * public void notifyCalled(String errorMessage) {
39 * mErrorMessage = errorMessage;
40 * // It's important to call this after all parameter assignments.
45 * // There should be one CallbackHelper instance per method.
46 * private OnOperationFailedHelper mOnOperationFailedHelper;
47 * private CallbackHelper mOnDataPersistedHelper;
49 * public OnOperationFailedHelper getOnOperationFailedHelper() {
50 * return mOnOperationFailedHelper;
53 * public CallbackHelper getOnDataPersistedHelper() {
54 * return mOnDataPersistedHelper;
58 * public void onOperationFailed(String errorMessage) {
59 * mOnOperationFailedHelper.notifyCalled(errorMessage);
63 * public void onDataPersisted() {
64 * mOnDataPersistedHelper.notifyCalled();
68 * // This is a sample test case.
69 * public void testCase() throws Exception {
70 * // Create the TestDelegate to inject into production code.
71 * TestDelegate delegate = new TestDelegate();
72 * // Create the production class instance that is being tested and inject the test delegate.
73 * CodeUnderTest codeUnderTest = new CodeUnderTest();
74 * codeUnderTest.setDelegate(delegate);
76 * // Typically you'd get the current call count before performing the operation you expect to
77 * // trigger the callback. There can't be any callbacks 'in flight' at this moment, otherwise
78 * // the call count is unpredictable and the test will be flaky.
79 * int onOperationFailedCallCount = delegate.getOnOperationFailedHelper().getCallCount();
80 * codeUnderTest.doSomethingThatEndsUpCallingOnOperationFailedFromAnotherThread();
81 * // It's safe to do other stuff here, if needed.
83 * // Wait for the callback if it hadn't been called yet, otherwise return immediately. This
84 * // can throw an exception if the callback doesn't arrive within the timeout.
85 * delegate.getOnOperationFailedHelper().waitForCallback(onOperationFailedCallCount);
86 * // Access to method parameters is now safe.
87 * assertEquals("server error", delegate.getOnOperationFailedHelper().getErrorMessage());
89 * // Being able to pass the helper around lets us build methods which encapsulate commonly
91 * doSomeOperationAndWait(codeUnerTest, delegate.getOnOperationFailedHelper());
93 * // The helper can be resued for as many calls as needed, just be sure to get the count each
95 * onOperationFailedCallCount = delegate.getOnOperationFailedHelper().getCallCount();
96 * codeUnderTest.doSomethingElseButStillFailOnAnotherThread();
97 * delegate.getOnOperationFailedHelper().waitForCallback(onOperationFailedCallCount);
99 * // It is also possible to use more than one helper at a time.
100 * onOperationFailedCallCount = delegate.getOnOperationFailedHelper().getCallCount();
101 * int onDataPersistedCallCount = delegate.getOnDataPersistedHelper().getCallCount();
102 * codeUnderTest.doSomethingThatPersistsDataButFailsInSomeOtherWayOnAnotherThread();
103 * delegate.getOnDataPersistedHelper().waitForCallback(onDataPersistedCallCount);
104 * delegate.getOnOperationFailedHelper().waitForCallback(onOperationFailedCallCount);
107 * // Shows how to turn an async operation + completion callback into a synchronous operation.
108 * private void doSomeOperationAndWait(final CodeUnderTest underTest,
109 * CallbackHelper operationHelper) throws InterruptedException, TimeoutException {
110 * final int callCount = operaitonHelper.getCallCount();
111 * getInstrumentaiton().runOnMainSync(new Runnable() {
113 * public void run() {
114 * // This schedules a call to a method on the injected TestDelegate. The TestDelegate
115 * // implementation will then call operationHelper.notifyCalled().
116 * underTest.operation();
119 * operationHelper.waitForCallback(callCount);
123 public class CallbackHelper {
124 protected static final int WAIT_TIMEOUT_SECONDS = 5;
126 private final Object mLock = new Object();
127 private int mCallCount = 0;
130 * Gets the number of times the callback has been called.
132 * The call count can be used with the waitForCallback() method, indicating a point
133 * in time after which the caller wishes to record calls to the callback.
135 * In order to wait for a callback caused by X, the call count should be obtained
138 * NOTE: any call to the callback that occurs after the call count is obtained
139 * will result in the corresponding wait call to resume execution. The call count
140 * is intended to 'catch' callbacks that occur after X but before waitForCallback()
143 public int getCallCount() {
144 synchronized(mLock) {
150 * Blocks until the callback is called the specified number of
151 * times or throws an exception if we exceeded the specified time frame.
153 * This will wait for a callback to be called a specified number of times after
154 * the point in time at which the call count was obtained. The method will return
155 * immediately if a call occurred the specified number of times after the
156 * call count was obtained but before the method was called, otherwise the method will
157 * block until the specified call count is reached.
159 * @param currentCallCount the value obtained by calling getCallCount().
160 * @param numberOfCallsToWaitFor number of calls (counting since
161 * currentCallCount was obtained) that we will wait for.
162 * @param timeout timeout value. We will wait the specified amount of time for a single
163 * callback to occur so the method call may block up to
164 * <code>numberOfCallsToWaitFor * timeout</code> units.
165 * @param unit timeout unit.
166 * @throws InterruptedException
167 * @throws TimeoutException Thrown if the method times out before onPageFinished is called.
169 public void waitForCallback(int currentCallCount, int numberOfCallsToWaitFor, long timeout,
170 TimeUnit unit) throws InterruptedException, TimeoutException {
171 assert mCallCount >= currentCallCount;
172 assert numberOfCallsToWaitFor > 0;
173 synchronized(mLock) {
174 int callCountWhenDoneWaiting = currentCallCount + numberOfCallsToWaitFor;
175 while (callCountWhenDoneWaiting > mCallCount) {
176 int callCountBeforeWait = mCallCount;
177 mLock.wait(unit.toMillis(timeout));
178 if (callCountBeforeWait == mCallCount) {
179 throw new TimeoutException("waitForCallback timed out!");
185 public void waitForCallback(int currentCallCount, int numberOfCallsToWaitFor)
186 throws InterruptedException, TimeoutException {
187 waitForCallback(currentCallCount, numberOfCallsToWaitFor,
188 WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
191 public void waitForCallback(int currentCallCount)
192 throws InterruptedException, TimeoutException {
193 waitForCallback(currentCallCount, 1);
197 * Blocks until the criteria is satisfied or throws an exception
198 * if the specified time frame is exceeded.
199 * @param timeout timeout value.
200 * @param unit timeout unit.
202 public void waitUntilCriteria(Criteria criteria, long timeout, TimeUnit unit)
203 throws InterruptedException, TimeoutException {
204 synchronized(mLock) {
205 final long startTime = System.currentTimeMillis();
206 boolean isSatisfied = criteria.isSatisfied();
207 while (!isSatisfied &&
208 System.currentTimeMillis() - startTime < unit.toMillis(timeout)) {
209 mLock.wait(unit.toMillis(timeout));
210 isSatisfied = criteria.isSatisfied();
212 if (!isSatisfied) throw new TimeoutException("waitUntilCriteria timed out!");
216 public void waitUntilCriteria(Criteria criteria)
217 throws InterruptedException, TimeoutException {
218 waitUntilCriteria(criteria, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
222 * Should be called when the callback associated with this helper object is called.
224 public void notifyCalled() {
225 synchronized(mLock) {