CallbackHelper.java revision 5d1f7b1de12d16ceb2c938c56701a3e8bfa558f7
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 java.util.concurrent.TimeUnit; 10import java.util.concurrent.TimeoutException; 11 12/** 13 * A helper class that encapsulates listening and blocking for callbacks. 14 * 15 * Sample usage: 16 * 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(); 25 * } 26 * 27 * // This is the inner class you'd write in your test case to later inject into the production 28 * // code. 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; 34 * 35 * public void getErrorMessage() { 36 * assert getCallCount() > 0; 37 * return mErrorMessage; 38 * } 39 * 40 * public void notifyCalled(String errorMessage) { 41 * mErrorMessage = errorMessage; 42 * // It's important to call this after all parameter assignments. 43 * notifyCalled(); 44 * } 45 * } 46 * 47 * // There should be one CallbackHelper instance per method. 48 * private OnOperationFailedHelper mOnOperationFailedHelper; 49 * private CallbackHelper mOnDataPersistedHelper; 50 * 51 * public OnOperationFailedHelper getOnOperationFailedHelper() { 52 * return mOnOperationFailedHelper; 53 * } 54 * 55 * public CallbackHelper getOnDataPersistedHelper() { 56 * return mOnDataPersistedHelper; 57 * } 58 * 59 * @Override 60 * public void onOperationFailed(String errorMessage) { 61 * mOnOperationFailedHelper.notifyCalled(errorMessage); 62 * } 63 * 64 * @Override 65 * public void onDataPersisted() { 66 * mOnDataPersistedHelper.notifyCalled(); 67 * } 68 * } 69 * 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); 77 * 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. 84 * .... 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()); 90 * 91 * // Being able to pass the helper around lets us build methods which encapsulate commonly 92 * // performed tasks. 93 * doSomeOperationAndWait(codeUnerTest, delegate.getOnOperationFailedHelper()); 94 * 95 * // The helper can be resued for as many calls as needed, just be sure to get the count each 96 * // time. 97 * onOperationFailedCallCount = delegate.getOnOperationFailedHelper().getCallCount(); 98 * codeUnderTest.doSomethingElseButStillFailOnAnotherThread(); 99 * delegate.getOnOperationFailedHelper().waitForCallback(onOperationFailedCallCount); 100 * 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); 107 * } 108 * 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() { 114 * @Override 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(); 119 * } 120 * }); 121 * operationHelper.waitForCallback(callCount); 122 * } 123 * 124 */ 125public class CallbackHelper { 126 protected static final long WAIT_TIMEOUT_SECONDS = scaleTimeout(5); 127 128 private final Object mLock = new Object(); 129 private int mCallCount = 0; 130 131 /** 132 * Gets the number of times the callback has been called. 133 * 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. 136 * 137 * In order to wait for a callback caused by X, the call count should be obtained 138 * before X occurs. 139 * 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() 143 * is called. 144 */ 145 public int getCallCount() { 146 synchronized (mLock) { 147 return mCallCount; 148 } 149 } 150 151 /** 152 * Blocks until the callback is called the specified number of 153 * times or throws an exception if we exceeded the specified time frame. 154 * 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. 160 * 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. 170 */ 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!"); 182 } 183 } 184 } 185 } 186 187 public void waitForCallback(int currentCallCount, int numberOfCallsToWaitFor) 188 throws InterruptedException, TimeoutException { 189 waitForCallback(currentCallCount, numberOfCallsToWaitFor, 190 WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); 191 } 192 193 public void waitForCallback(int currentCallCount) 194 throws InterruptedException, TimeoutException { 195 waitForCallback(currentCallCount, 1); 196 } 197 198 /** 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. 203 */ 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(); 213 } 214 if (!isSatisfied) throw new TimeoutException("waitUntilCriteria timed out!"); 215 } 216 } 217 218 public void waitUntilCriteria(Criteria criteria) 219 throws InterruptedException, TimeoutException { 220 waitUntilCriteria(criteria, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); 221 } 222 223 /** 224 * Should be called when the callback associated with this helper object is called. 225 */ 226 public void notifyCalled() { 227 synchronized (mLock) { 228 mCallCount++; 229 mLock.notifyAll(); 230 } 231 } 232} 233