1 // Copyright 2012 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.base.test.util; 6 7 import org.hamcrest.MatcherAssert; 8 import org.hamcrest.Matchers; 9 import org.junit.Assert; 10 11 import java.util.concurrent.TimeUnit; 12 import 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 reused 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 = operationHelper.getCallCount(); 115 * getInstrumentation().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 */ 127 public class CallbackHelper { 128 /** The default timeout (in seconds) for a callback to wait. */ 129 public static final long WAIT_TIMEOUT_SECONDS = 5L; 130 131 private final Object mLock = new Object(); 132 private int mCallCount; 133 private String mFailureString; 134 private boolean mSingleShotMode; 135 136 /** 137 * Gets the number of times the callback has been called. 138 * 139 * The call count can be used with the waitForCallback() method, indicating a point 140 * in time after which the caller wishes to record calls to the callback. 141 * 142 * In order to wait for a callback caused by X, the call count should be obtained 143 * before X occurs. 144 * 145 * NOTE: any call to the callback that occurs after the call count is obtained 146 * will result in the corresponding wait call to resume execution. The call count 147 * is intended to 'catch' callbacks that occur after X but before waitForCallback() 148 * is called. 149 */ getCallCount()150 public int getCallCount() { 151 synchronized (mLock) { 152 return mCallCount; 153 } 154 } 155 156 /** 157 * Blocks until the callback is called the specified number of 158 * times or throws an exception if we exceeded the specified time frame. 159 * 160 * This will wait for a callback to be called a specified number of times after 161 * the point in time at which the call count was obtained. The method will return 162 * immediately if a call occurred the specified number of times after the 163 * call count was obtained but before the method was called, otherwise the method will 164 * block until the specified call count is reached. 165 * 166 * @param msg The error message to use if the callback times out. 167 * @param currentCallCount Wait until |notifyCalled| has been called this many times in total. 168 * @param numberOfCallsToWaitFor number of calls (counting since 169 * currentCallCount was obtained) that we will wait for. 170 * @param timeout timeout value for all callbacks to occur. 171 * @param unit timeout unit. 172 * @throws TimeoutException Thrown if the method times out before onPageFinished is called. 173 */ waitForCallback(String msg, int currentCallCount, int numberOfCallsToWaitFor, long timeout, TimeUnit unit)174 public void waitForCallback(String msg, int currentCallCount, int numberOfCallsToWaitFor, 175 long timeout, TimeUnit unit) throws TimeoutException { 176 assert mCallCount >= currentCallCount; 177 assert numberOfCallsToWaitFor > 0; 178 TimeoutTimer timer = new TimeoutTimer(unit.toMillis(timeout)); 179 synchronized (mLock) { 180 int callCountWhenDoneWaiting = currentCallCount + numberOfCallsToWaitFor; 181 while (callCountWhenDoneWaiting > mCallCount && !timer.isTimedOut()) { 182 try { 183 mLock.wait(timer.getRemainingMs()); 184 } catch (InterruptedException e) { 185 // Ignore the InterruptedException. Rely on the outer while loop to re-run. 186 } 187 if (mFailureString != null) { 188 String s = mFailureString; 189 mFailureString = null; 190 Assert.fail(s); 191 } 192 } 193 if (timer.isTimedOut()) { 194 throw new TimeoutException(msg == null ? "waitForCallback timed out!" : msg); 195 } 196 } 197 } 198 199 /** 200 * @see #waitForCallback(String, int, int, long, TimeUnit) 201 */ waitForCallback(int currentCallCount, int numberOfCallsToWaitFor, long timeout, TimeUnit unit)202 public void waitForCallback(int currentCallCount, int numberOfCallsToWaitFor, long timeout, 203 TimeUnit unit) throws TimeoutException { 204 waitForCallback(null, currentCallCount, numberOfCallsToWaitFor, timeout, unit); 205 } 206 207 /** 208 * @see #waitForCallback(String, int, int, long, TimeUnit) 209 */ waitForCallback(int currentCallCount, int numberOfCallsToWaitFor)210 public void waitForCallback(int currentCallCount, int numberOfCallsToWaitFor) 211 throws TimeoutException { 212 waitForCallback(null, currentCallCount, numberOfCallsToWaitFor, 213 WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); 214 } 215 216 /** 217 * @see #waitForCallback(String, int, int, long, TimeUnit) 218 */ waitForCallback(String msg, int currentCallCount)219 public void waitForCallback(String msg, int currentCallCount) throws TimeoutException { 220 waitForCallback(msg, currentCallCount, 1, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); 221 } 222 223 /** 224 * @see #waitForCallback(String, int, int, long, TimeUnit) 225 */ waitForCallback(int currentCallCount)226 public void waitForCallback(int currentCallCount) throws TimeoutException { 227 waitForCallback(null, currentCallCount, 1, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); 228 } 229 230 /** 231 * Blocks until the next time the callback is called. 232 * @param msg The error message to use if the callback times out. 233 * @throws TimeoutException 234 */ waitForNext(String msg)235 public void waitForNext(String msg) throws TimeoutException { 236 waitForCallback(msg, mCallCount, 1, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); 237 } 238 239 /** @see #waitForNext(String) */ waitForNext()240 public void waitForNext() throws TimeoutException { 241 waitForNext(null); 242 } 243 244 /** 245 * Blocks until the next time the callback is called. 246 * @param timeout timeout value for all callbacks to occur. 247 * @param unit timeout unit. 248 * @throws TimeoutException 249 */ waitForNext(long timeout, TimeUnit unit)250 public void waitForNext(long timeout, TimeUnit unit) throws TimeoutException { 251 waitForCallback(null, mCallCount, 1, timeout, unit); 252 } 253 254 /** 255 * Wait until the callback has been called once. 256 */ waitForFirst(String msg, long timeout, TimeUnit unit)257 public void waitForFirst(String msg, long timeout, TimeUnit unit) throws TimeoutException { 258 MatcherAssert.assertThat( 259 "Use waitForCallback(currentCallCount) for callbacks that are called multiple " 260 + "times.", 261 mCallCount, Matchers.lessThanOrEqualTo(1)); 262 mSingleShotMode = true; 263 waitForCallback(msg, 0, 1, timeout, unit); 264 } 265 266 /** 267 * Wait until the callback has been called once. 268 */ waitForFirst(long timeout, TimeUnit unit)269 public void waitForFirst(long timeout, TimeUnit unit) throws TimeoutException { 270 waitForFirst(null, timeout, unit); 271 } 272 273 /** 274 * Wait until the callback has been called once. 275 */ waitForFirst(String msg)276 public void waitForFirst(String msg) throws TimeoutException { 277 waitForFirst(msg, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); 278 } 279 280 /** 281 * Wait until the callback has been called at least once. 282 */ waitForFirst()283 public void waitForFirst() throws TimeoutException { 284 waitForFirst(null); 285 } 286 287 /** 288 * Should be called when the callback associated with this helper object is called. 289 */ notifyCalled()290 public void notifyCalled() { 291 notifyInternal(null); 292 } 293 294 /** 295 * Should be called when the callback associated with this helper object wants to 296 * indicate a failure. 297 * 298 * @param s The failure message. 299 */ notifyFailed(String s)300 public void notifyFailed(String s) { 301 notifyInternal(s); 302 } 303 notifyInternal(String failureString)304 private void notifyInternal(String failureString) { 305 synchronized (mLock) { 306 mCallCount++; 307 mFailureString = failureString; 308 if (mSingleShotMode && mCallCount > 1) { 309 Assert.fail("Single-use callback called multiple times."); 310 } 311 mLock.notifyAll(); 312 } 313 } 314 } 315