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 | } |