blob: 1b56e2a77c7ad490c4539d0e82740433fbe71821 [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base.test.util;
import androidx.annotation.Nullable;
import org.junit.Assert;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* A generic wrapper around {@link CallbackHelper} that takes an object when notified. Very often
* tests pass a {@link org.chromium.base.Callback} which will be given a payload by the production
* code, and the tests want to assert something about this payload. This class aims to reduce the
* number of identical subclasses used to temporarily hold onto that payload.
*
* Sample usage:
*
* private interface ComplexSignature {
* void onResult(Object obj, Integer Int, Boolean bool);
* }
*
* private interface ClassUnderTest {
* void getSimpleAsync(Callback<Object> callback);
* void getComplexAsync(ComplexSignature callback);
* }
*
* // Typically the callback can be wired with simply a method reference.
* @Test
* public void testGetSimpleAsync() {
* ClassUnderTest testMe = initClassUnderTest();
* PayloadCallbackHelper<Object> callbackHelper = new PayloadCallbackHelper<>();
* testMe.getSimpleAsync(callbackHelper::notifyCalled);
* Assert.assertNotNull(callbackHelper.getOnlyPayloadBlocking());
* }
*
* // Sometimes the method signature will be messier and you'll want a lambda.
* @Test
* public void testGetComplexAsync() {
* ClassUnderTest testMe = initClassUnderTest();
* PayloadCallbackHelper<Object> callbackHelper = new PayloadCallbackHelper<>();
* testMe.getComplexAsync((Object obj, Integer ignored1, Boolean ignored2) ->
* callbackHelper.notifyCalled(obj));
* Assert.assertNotNull(callbackHelper.getOnlyPayloadBlocking());
* }
*
* @param <T> The type of object to be notified with.
*/
public class PayloadCallbackHelper<T> {
private final List<T> mPayloadList = Collections.synchronizedList(new ArrayList<>());
private final CallbackHelper mDelegate = new CallbackHelper();
/**
* Embed this method inside external callbacks to monitor when they are called.
* @param payload The payload object to store for verification.
*/
public void notifyCalled(T payload) {
mPayloadList.add(payload);
mDelegate.notifyCalled();
}
/**
* Blocks until the requested payload is provided, and then returns it.
* @param index Index into a conceptual array of payloads provided by sequential callbacks.
* @return The nth payload provided to notify. Null is a valid return value if the callback was
* invoked with null.
* @throws IndexOutOfBoundsException If notify is not called at least the specified number of
* times.
*/
@Nullable
public T getPayloadByIndexBlocking(int index) {
return getPayloadByIndexBlocking(
index, CallbackHelper.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
}
/**
* Blocks until the requested payload is provided, and then returns it.
* @param index Index into a conceptual array of payloads provided by sequential callbacks.
* @param timeout timeout value for all callbacks to occur.
* @param unit timeout unit.
* @return The nth payload provided to notify. Null is a valid return value if the callback was
* invoked with null.
* @throws IndexOutOfBoundsException If notify is not called at least the specified number of
* times.
*/
@Nullable
public T getPayloadByIndexBlocking(int index, long timeout, TimeUnit unit) {
waitForCallback(1 + index, timeout, unit);
return mPayloadList.get(index);
}
/**
* Returns the payload, blocking if notify has not been called yet. Verifies that {@link
* #notifyCalled} has only been invoked once.
* @return The payload provided to notify. Null is a valid return value if the callback was
* invoked with null.
* @throws IndexOutOfBoundsException If notify is never called.
*/
@Nullable
public T getOnlyPayloadBlocking() {
waitForCallback(1, CallbackHelper.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
// While this lock likely isn't necessary for tests to call this method correctly, it allows
// this method to truly fulfil the contact promised by the method's name that there's only
// one payload. Other threads may be waiting to notify while this lock is held. Note this
// lock is the same Collections#synchronizedList is using.
synchronized (mPayloadList) {
Assert.assertEquals(1, mPayloadList.size());
return mPayloadList.get(0);
}
}
/**
* @return The number of times notify has been called.
*/
public int getCallCount() {
return mDelegate.getCallCount();
}
/**
* Blocks until notify has been called the specified number of times.
* @param expectedCallCount The number of times notify should be called.
* @param timeout timeout value for all callbacks to occur.
* @param unit timeout unit.
* @throws IndexOutOfBoundsException If notify is not called at least the specified number of
* times.
*/
private void waitForCallback(int expectedCallCount, long timeout, TimeUnit unit) {
int currentCallCount = mDelegate.getCallCount();
int numberOfCallsToWaitFor = expectedCallCount - currentCallCount;
if (numberOfCallsToWaitFor <= 0) {
return;
}
try {
mDelegate.waitForCallback(currentCallCount, numberOfCallsToWaitFor, timeout, unit);
} catch (TimeoutException te) {
throw new IllegalStateException(te);
}
}
}