blob: 5c5bca91214f4ae661506040593bf7b8a686b26a [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base.process_launcher;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.content.ComponentName;
import android.content.Context;
import android.os.Bundle;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Feature;
import java.util.HashSet;
import java.util.Set;
/** Unit tests for the ChildConnectionAllocator class. */
@Config(manifest = Config.NONE)
@RunWith(BaseRobolectricTestRunner.class)
public class ChildConnectionAllocatorTest {
private static final String TEST_PACKAGE_NAME = "org.chromium.allocator_test";
private static final int MAX_CONNECTION_NUMBER = 2;
private static final int FREE_CONNECTION_TEST_CALLBACK_START_FAILED = 1;
private static final int FREE_CONNECTION_TEST_CALLBACK_PROCESS_DIED = 2;
@Mock
private ChildProcessConnection.ServiceCallback mServiceCallback;
static class TestConnectionFactory implements ChildConnectionAllocator.ConnectionFactory {
private ComponentName mLastServiceName;
private ChildProcessConnection mConnection;
private ChildProcessConnection.ServiceCallback mConnectionServiceCallback;
@Override
public ChildProcessConnection createConnection(Context context, ComponentName serviceName,
boolean bindToCaller, boolean bindAsExternalService, Bundle serviceBundle) {
mLastServiceName = serviceName;
if (mConnection == null) {
mConnection = mock(ChildProcessConnection.class);
// Retrieve the ServiceCallback so we can simulate the service process dying.
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) {
mConnectionServiceCallback =
(ChildProcessConnection.ServiceCallback) invocation.getArgument(1);
return null;
}
})
.when(mConnection)
.start(anyBoolean(), any(ChildProcessConnection.ServiceCallback.class));
}
return mConnection;
}
public ComponentName getAndResetLastServiceName() {
ComponentName serviceName = mLastServiceName;
mLastServiceName = null;
return serviceName;
}
// Use this method to have a callback invoked when the connection is started on the next
// created connection.
public void invokeCallbackOnConnectionStart(final boolean onChildStarted,
final boolean onStartFailed, final boolean onChildProcessDied) {
final ChildProcessConnection connection = mock(ChildProcessConnection.class);
mConnection = connection;
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) {
ChildProcessConnection.ServiceCallback serviceCallback =
(ChildProcessConnection.ServiceCallback) invocation.getArgument(1);
if (onChildStarted) {
serviceCallback.onChildStarted();
}
if (onStartFailed) {
serviceCallback.onChildStartFailed(connection);
}
if (onChildProcessDied) {
serviceCallback.onChildProcessDied(connection);
}
return null;
}
})
.when(mConnection)
.start(anyBoolean(), any(ChildProcessConnection.ServiceCallback.class));
}
public void simulateServiceStartFailed() {
mConnectionServiceCallback.onChildStartFailed(mConnection);
}
public void simulateServiceProcessDying() {
mConnectionServiceCallback.onChildProcessDied(mConnection);
}
}
private final TestConnectionFactory mTestConnectionFactory = new TestConnectionFactory();
private ChildConnectionAllocator mAllocator;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mAllocator = ChildConnectionAllocator.createForTest(null, TEST_PACKAGE_NAME,
"AllocatorTest", MAX_CONNECTION_NUMBER, true /* bindToCaller */,
false /* bindAsExternalService */, false /* useStrongBinding */);
mAllocator.setConnectionFactoryForTesting(mTestConnectionFactory);
}
@Test
@Feature({"ProcessManagement"})
public void testPlainAllocate() {
assertFalse(mAllocator.anyConnectionAllocated());
assertEquals(MAX_CONNECTION_NUMBER, mAllocator.getNumberOfServices());
ChildProcessConnection connection =
mAllocator.allocate(null /* context */, null /* serviceBundle */, mServiceCallback);
assertNotNull(connection);
verify(connection, times(1))
.start(eq(false) /* useStrongBinding */,
any(ChildProcessConnection.ServiceCallback.class));
assertTrue(mAllocator.anyConnectionAllocated());
}
/** Tests that different services are created until we reach the max number specified. */
@Test
@Feature({"ProcessManagement"})
public void testAllocateMaxNumber() {
assertTrue(mAllocator.isFreeConnectionAvailable());
Set<ComponentName> serviceNames = new HashSet<>();
for (int i = 0; i < MAX_CONNECTION_NUMBER; i++) {
ChildProcessConnection connection = mAllocator.allocate(
null /* context */, null /* serviceBundle */, mServiceCallback);
assertNotNull(connection);
ComponentName serviceName = mTestConnectionFactory.getAndResetLastServiceName();
assertFalse(serviceNames.contains(serviceName));
serviceNames.add(serviceName);
}
assertFalse(mAllocator.isFreeConnectionAvailable());
assertNull(mAllocator.allocate(
null /* context */, null /* serviceBundle */, mServiceCallback));
}
@Test
@Feature({"ProcessManagement"})
public void testQueueAllocation() {
Runnable freeConnectionCallback = mock(Runnable.class);
mAllocator = ChildConnectionAllocator.createForTest(freeConnectionCallback,
TEST_PACKAGE_NAME, "AllocatorTest", 1, true /* bindToCaller */,
false /* bindAsExternalService */, false /* useStrongBinding */);
mAllocator.setConnectionFactoryForTesting(mTestConnectionFactory);
// Occupy all slots.
ChildProcessConnection connection =
mAllocator.allocate(null /* context */, null /* serviceBundle */, mServiceCallback);
assertNotNull(connection);
assertFalse(mAllocator.isFreeConnectionAvailable());
final ChildProcessConnection newConnection[] = new ChildProcessConnection[2];
Runnable allocate1 = () -> {
newConnection[0] = mAllocator.allocate(
null /* context */, null /* serviceBundle */, mServiceCallback);
};
Runnable allocate2 = () -> {
newConnection[1] = mAllocator.allocate(
null /* context */, null /* serviceBundle */, mServiceCallback);
};
mAllocator.queueAllocation(allocate1);
mAllocator.queueAllocation(allocate2);
verify(freeConnectionCallback, times(1)).run();
assertNull(newConnection[0]);
mTestConnectionFactory.simulateServiceProcessDying();
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
assertNotNull(newConnection[0]);
assertNull(newConnection[1]);
mTestConnectionFactory.simulateServiceProcessDying();
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
assertNotNull(newConnection[1]);
}
/**
* Tests that the connection is created with the useStrongBinding parameter specified in the
* allocator.
*/
@Test
@Feature({"ProcessManagement"})
public void testStrongBindingParam() {
for (boolean useStrongBinding : new boolean[] {true, false}) {
ChildConnectionAllocator allocator = ChildConnectionAllocator.createForTest(null,
TEST_PACKAGE_NAME, "AllocatorTest", MAX_CONNECTION_NUMBER,
true /* bindToCaller */, false /* bindAsExternalService */, useStrongBinding);
allocator.setConnectionFactoryForTesting(mTestConnectionFactory);
ChildProcessConnection connection = allocator.allocate(
null /* context */, null /* serviceBundle */, mServiceCallback);
verify(connection, times(0)).start(useStrongBinding, mServiceCallback);
}
}
/**
* Tests that the various ServiceCallbacks are propagated and posted, so they happen after the
* ChildProcessAllocator,allocate() method has returned.
*/
public void runTestWithConnectionCallbacks(
boolean onChildStarted, boolean onChildStartFailed, boolean onChildProcessDied) {
// We have to pause the Roboletric looper or it'll execute the posted tasks synchronoulsy.
ShadowLooper.pauseMainLooper();
mTestConnectionFactory.invokeCallbackOnConnectionStart(
onChildStarted, onChildStartFailed, onChildProcessDied);
ChildProcessConnection connection =
mAllocator.allocate(null /* context */, null /* serviceBundle */, mServiceCallback);
assertNotNull(connection);
// Callbacks are posted.
verify(mServiceCallback, never()).onChildStarted();
verify(mServiceCallback, never()).onChildStartFailed(any());
verify(mServiceCallback, never()).onChildProcessDied(any());
ShadowLooper.unPauseMainLooper();
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
verify(mServiceCallback, times(onChildStarted ? 1 : 0)).onChildStarted();
verify(mServiceCallback, times(onChildStartFailed ? 1 : 0)).onChildStartFailed(any());
verify(mServiceCallback, times(onChildProcessDied ? 1 : 0)).onChildProcessDied(any());
}
@Test
@Feature({"ProcessManagement"})
public void testOnChildStartedCallback() {
runTestWithConnectionCallbacks(true /* onChildStarted */, false /* onChildStartFailed */,
false /* onChildProcessDied */);
}
@Test
@Feature({"ProcessManagement"})
public void testOnChildStartFailedCallback() {
runTestWithConnectionCallbacks(false /* onChildStarted */, true /* onChildStartFailed */,
false /* onChildProcessDied */);
}
@Test
@Feature({"ProcessManagement"})
public void testOnChildProcessDiedCallback() {
runTestWithConnectionCallbacks(false /* onChildStarted */, false /* onChildStartFailed */,
true /* onChildProcessDied */);
}
/**
* Tests that the allocator clears the connection when it fails to bind/process dies.
*/
private void testFreeConnection(int callbackType) {
ChildProcessConnection connection =
mAllocator.allocate(null /* context */, null /* serviceBundle */, mServiceCallback);
assertNotNull(connection);
ComponentName serviceName = mTestConnectionFactory.getAndResetLastServiceName();
verify(connection, times(1))
.start(eq(false) /* useStrongBinding */,
any(ChildProcessConnection.ServiceCallback.class));
assertTrue(mAllocator.anyConnectionAllocated());
int onChildStartFailedExpectedCount = 0;
int onChildProcessDiedExpectedCount = 0;
switch (callbackType) {
case FREE_CONNECTION_TEST_CALLBACK_START_FAILED:
mTestConnectionFactory.simulateServiceStartFailed();
onChildStartFailedExpectedCount = 1;
break;
case FREE_CONNECTION_TEST_CALLBACK_PROCESS_DIED:
mTestConnectionFactory.simulateServiceProcessDying();
onChildProcessDiedExpectedCount = 1;
break;
default:
fail();
break;
}
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
assertFalse(mAllocator.anyConnectionAllocated());
verify(mServiceCallback, never()).onChildStarted();
verify(mServiceCallback, times(onChildStartFailedExpectedCount))
.onChildStartFailed(connection);
verify(mServiceCallback, times(onChildProcessDiedExpectedCount))
.onChildProcessDied(connection);
// Allocate a new connection to make sure we are not getting the same connection.
connection =
mAllocator.allocate(null /* context */, null /* serviceBundle */, mServiceCallback);
assertNotNull(connection);
assertNotEquals(mTestConnectionFactory.getAndResetLastServiceName(), serviceName);
}
@Test
@Feature({"ProcessManagement"})
public void testFreeConnectionOnChildStartFailed() {
testFreeConnection(FREE_CONNECTION_TEST_CALLBACK_START_FAILED);
}
@Test
@Feature({"ProcessManagement"})
public void testFreeConnectionOnChildProcessDied() {
testFreeConnection(FREE_CONNECTION_TEST_CALLBACK_PROCESS_DIED);
}
}