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