• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2021 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 androidx.annotation.Nullable;
8 
9 import org.junit.Assert;
10 
11 import java.util.ArrayList;
12 import java.util.Collections;
13 import java.util.List;
14 import java.util.concurrent.TimeUnit;
15 import java.util.concurrent.TimeoutException;
16 
17 /**
18  * A generic wrapper around {@link CallbackHelper} that takes an object when notified. Very often
19  * tests pass a {@link org.chromium.base.Callback} which will be given a payload by the production
20  * code, and the tests want to assert something about this payload. This class aims to reduce the
21  * number of identical subclasses used to temporarily hold onto that payload.
22  *
23  * Sample usage:
24  *
25  * private interface ComplexSignature {
26  *     void onResult(Object obj, Integer Int, Boolean bool);
27  * }
28  *
29  * private interface ClassUnderTest {
30  *     void getSimpleAsync(Callback<Object> callback);
31  *     void getComplexAsync(ComplexSignature callback);
32  * }
33  *
34  * // Typically the callback can be wired with simply a method reference.
35  * @Test
36  * public void testGetSimpleAsync() {
37  *     ClassUnderTest testMe = initClassUnderTest();
38  *     PayloadCallbackHelper<Object> callbackHelper = new PayloadCallbackHelper<>();
39  *     testMe.getSimpleAsync(callbackHelper::notifyCalled);
40  *     Assert.assertNotNull(callbackHelper.getOnlyPayloadBlocking());
41  * }
42  *
43  * // Sometimes the method signature will be messier and you'll want a lambda.
44  * @Test
45  * public void testGetComplexAsync() {
46  *     ClassUnderTest testMe = initClassUnderTest();
47  *     PayloadCallbackHelper<Object> callbackHelper = new PayloadCallbackHelper<>();
48  *     testMe.getComplexAsync((Object obj, Integer ignored1, Boolean ignored2) ->
49  *         callbackHelper.notifyCalled(obj));
50  *     Assert.assertNotNull(callbackHelper.getOnlyPayloadBlocking());
51  * }
52  *
53  * @param <T> The type of object to be notified with.
54  */
55 public class PayloadCallbackHelper<T> {
56     private final List<T> mPayloadList = Collections.synchronizedList(new ArrayList<>());
57     private final CallbackHelper mDelegate = new CallbackHelper();
58 
59     /**
60      * Embed this method inside external callbacks to monitor when they are called.
61      * @param payload The payload object to store for verification.
62      */
notifyCalled(T payload)63     public void notifyCalled(T payload) {
64         mPayloadList.add(payload);
65         mDelegate.notifyCalled();
66     }
67 
68     /**
69      * Blocks until the requested payload is provided, and then returns it.
70      * @param index Index into a conceptual array of payloads provided by sequential callbacks.
71      * @return The nth payload provided to notify. Null is a valid return value if the callback was
72      *         invoked with null.
73      * @throws IndexOutOfBoundsException If notify is not called at least the specified number of
74      *         times.
75      */
76     @Nullable
getPayloadByIndexBlocking(int index)77     public T getPayloadByIndexBlocking(int index) {
78         return getPayloadByIndexBlocking(
79                 index, CallbackHelper.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
80     }
81 
82     /**
83      * Blocks until the requested payload is provided, and then returns it.
84      * @param index Index into a conceptual array of payloads provided by sequential callbacks.
85      * @param timeout timeout value for all callbacks to occur.
86      * @param unit timeout unit.
87      * @return The nth payload provided to notify. Null is a valid return value if the callback was
88      *         invoked with null.
89      * @throws IndexOutOfBoundsException If notify is not called at least the specified number of
90      *         times.
91      */
92     @Nullable
getPayloadByIndexBlocking(int index, long timeout, TimeUnit unit)93     public T getPayloadByIndexBlocking(int index, long timeout, TimeUnit unit) {
94         waitForCallback(1 + index, timeout, unit);
95         return mPayloadList.get(index);
96     }
97 
98     /**
99      * Returns the payload, blocking if notify has not been called yet. Verifies that {@link
100      * #notifyCalled} has only been invoked once.
101      * @return The payload provided to notify. Null is a valid return value if the callback was
102      *         invoked with null.
103      * @throws IndexOutOfBoundsException If notify is never called.
104      */
105     @Nullable
getOnlyPayloadBlocking()106     public T getOnlyPayloadBlocking() {
107         waitForCallback(1, CallbackHelper.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
108         // While this lock likely isn't necessary for tests to call this method correctly, it allows
109         // this method to truly fulfil the contact promised by the method's name that there's only
110         // one payload. Other threads may be waiting to notify while this lock is held. Note this
111         // lock is the same Collections#synchronizedList is using.
112         synchronized (mPayloadList) {
113             Assert.assertEquals(1, mPayloadList.size());
114             return mPayloadList.get(0);
115         }
116     }
117 
118     /**
119      * @return The number of times notify has been called.
120      */
getCallCount()121     public int getCallCount() {
122         return mDelegate.getCallCount();
123     }
124 
125     /**
126      * Blocks until notify has been called the specified number of times.
127      * @param expectedCallCount The number of times notify should be called.
128      * @param timeout timeout value for all callbacks to occur.
129      * @param unit timeout unit.
130      * @throws IndexOutOfBoundsException If notify is not called at least the specified number of
131      *         times.
132      */
waitForCallback(int expectedCallCount, long timeout, TimeUnit unit)133     private void waitForCallback(int expectedCallCount, long timeout, TimeUnit unit) {
134         int currentCallCount = mDelegate.getCallCount();
135         int numberOfCallsToWaitFor = expectedCallCount - currentCallCount;
136         if (numberOfCallsToWaitFor <= 0) {
137             return;
138         }
139         try {
140             mDelegate.waitForCallback(currentCallCount, numberOfCallsToWaitFor, timeout, unit);
141         } catch (TimeoutException te) {
142             throw new IllegalStateException(te);
143         }
144     }
145 }
146