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