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