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