blob: a12d50d27e579e31c1e421f15cbe7c4ba95e4165 [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.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.AdditionalMatchers.or;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.ComponentName;
import android.content.Intent;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import org.junit.Assert;
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.ChildBindingState;
import org.chromium.base.test.BaseRobolectricTestRunner;
/** Unit tests for ChildProcessConnection. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ChildProcessConnectionTest {
private static class ChildServiceConnectionMock
implements ChildProcessConnection.ChildServiceConnection {
private final Intent mBindIntent;
private final ChildProcessConnection.ChildServiceConnectionDelegate mDelegate;
private boolean mBound;
public ChildServiceConnectionMock(
Intent bindIntent, ChildProcessConnection.ChildServiceConnectionDelegate delegate) {
mBindIntent = bindIntent;
mDelegate = delegate;
}
@Override
public boolean bind() {
mBound = true;
return true;
}
@Override
public void unbind() {
mBound = false;
}
@Override
public boolean isBound() {
return mBound;
}
public void notifyServiceConnected(IBinder service) {
mDelegate.onServiceConnected(service);
}
public void notifyServiceDisconnected() {
mDelegate.onServiceDisconnected();
}
public Intent getBindIntent() {
return mBindIntent;
}
};
private final ChildProcessConnection.ChildServiceConnectionFactory mServiceConnectionFactory =
new ChildProcessConnection.ChildServiceConnectionFactory() {
@Override
public ChildProcessConnection.ChildServiceConnection createConnection(
Intent bindIntent, int bindFlags,
ChildProcessConnection.ChildServiceConnectionDelegate delegate) {
ChildServiceConnectionMock connection =
spy(new ChildServiceConnectionMock(bindIntent, delegate));
if (mFirstServiceConnection == null) {
mFirstServiceConnection = connection;
}
return connection;
}
};
@Mock
private ChildProcessConnection.ServiceCallback mServiceCallback;
@Mock
private ChildProcessConnection.ConnectionCallback mConnectionCallback;
private IChildProcessService mIChildProcessService;
private Binder mChildProcessServiceBinder;
private ChildServiceConnectionMock mFirstServiceConnection;
// Parameters captured from the IChildProcessService.setupConnection() call
private Bundle mConnectionBundle;
private ICallbackInt mConnectionPidCallback;
private IBinder mConnectionIBinderCallback;
@Before
public void setUp() throws RemoteException {
MockitoAnnotations.initMocks(this);
mIChildProcessService = mock(IChildProcessService.class);
// Capture the parameters passed to the IChildProcessService.setupConnection() call.
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) {
mConnectionBundle = (Bundle) invocation.getArgument(0);
mConnectionPidCallback = (ICallbackInt) invocation.getArgument(1);
mConnectionIBinderCallback = (IBinder) invocation.getArgument(2);
return null;
}
})
.when(mIChildProcessService)
.setupConnection(
or(isNull(), any(Bundle.class)), or(isNull(), any()), or(isNull(), any()));
mChildProcessServiceBinder = new Binder();
mChildProcessServiceBinder.attachInterface(
mIChildProcessService, IChildProcessService.class.getName());
}
private ChildProcessConnection createDefaultTestConnection() {
return createTestConnection(false /* bindToCaller */, false /* bindAsExternalService */,
null /* serviceBundle */);
}
private ChildProcessConnection createTestConnection(
boolean bindToCaller, boolean bindAsExternalService, Bundle serviceBundle) {
String packageName = "org.chromium.test";
String serviceName = "TestService";
return new ChildProcessConnection(null /* context */,
new ComponentName(packageName, serviceName), bindToCaller, bindAsExternalService,
serviceBundle, mServiceConnectionFactory);
}
@Test
public void testStrongBinding() {
ChildProcessConnection connection = createDefaultTestConnection();
connection.start(true /* useStrongBinding */, null /* serviceCallback */);
assertTrue(connection.isStrongBindingBound());
connection = createDefaultTestConnection();
connection.start(false /* useStrongBinding */, null /* serviceCallback */);
assertFalse(connection.isStrongBindingBound());
}
@Test
public void testServiceBundle() {
Bundle serviceBundle = new Bundle();
final String intKey = "org.chromium.myInt";
final int intValue = 34;
final int defaultValue = -1;
serviceBundle.putInt(intKey, intValue);
String stringKey = "org.chromium.myString";
String stringValue = "thirty four";
serviceBundle.putString(stringKey, stringValue);
ChildProcessConnection connection = createTestConnection(
false /* bindToCaller */, false /* bindAsExternalService */, serviceBundle);
// Start the connection without the ChildServiceConnection connecting.
connection.start(false /* useStrongBinding */, null /* serviceCallback */);
assertNotNull(mFirstServiceConnection);
Intent bindIntent = mFirstServiceConnection.getBindIntent();
assertNotNull(bindIntent);
assertEquals(intValue, bindIntent.getIntExtra(intKey, defaultValue));
assertEquals(stringValue, bindIntent.getStringExtra(stringKey));
}
@Test
public void testServiceStartsSuccessfully() {
ChildProcessConnection connection = createDefaultTestConnection();
assertNotNull(mFirstServiceConnection);
connection.start(false /* useStrongBinding */, mServiceCallback);
Assert.assertTrue(connection.isModerateBindingBound());
Assert.assertFalse(connection.didOnServiceConnectedForTesting());
verify(mServiceCallback, never()).onChildStarted();
verify(mServiceCallback, never()).onChildStartFailed(any());
verify(mServiceCallback, never()).onChildProcessDied(any());
// The service connects.
mFirstServiceConnection.notifyServiceConnected(null /* iBinder */);
Assert.assertTrue(connection.didOnServiceConnectedForTesting());
verify(mServiceCallback, times(1)).onChildStarted();
verify(mServiceCallback, never()).onChildStartFailed(any());
verify(mServiceCallback, never()).onChildProcessDied(any());
}
@Test
public void testServiceStartsAndFailsToBind() {
ChildProcessConnection connection = createDefaultTestConnection();
assertNotNull(mFirstServiceConnection);
// Note we use doReturn so the actual bind() method is not called (it would with
// when(mFirstServiceConnection.bind()).thenReturn(false).
doReturn(false).when(mFirstServiceConnection).bind();
connection.start(false /* useStrongBinding */, mServiceCallback);
Assert.assertFalse(connection.isModerateBindingBound());
Assert.assertFalse(connection.didOnServiceConnectedForTesting());
verify(mServiceCallback, never()).onChildStarted();
verify(mServiceCallback, never()).onChildStartFailed(any());
verify(mServiceCallback, times(1)).onChildProcessDied(connection);
}
@Test
public void testServiceStops() {
ChildProcessConnection connection = createDefaultTestConnection();
assertNotNull(mFirstServiceConnection);
connection.start(false /* useStrongBinding */, mServiceCallback);
mFirstServiceConnection.notifyServiceConnected(null /* iBinder */);
connection.stop();
verify(mServiceCallback, times(1)).onChildStarted();
verify(mServiceCallback, never()).onChildStartFailed(any());
verify(mServiceCallback, times(1)).onChildProcessDied(connection);
}
@Test
public void testServiceDisconnects() {
ChildProcessConnection connection = createDefaultTestConnection();
assertNotNull(mFirstServiceConnection);
connection.start(false /* useStrongBinding */, mServiceCallback);
mFirstServiceConnection.notifyServiceConnected(null /* iBinder */);
mFirstServiceConnection.notifyServiceDisconnected();
verify(mServiceCallback, times(1)).onChildStarted();
verify(mServiceCallback, never()).onChildStartFailed(any());
verify(mServiceCallback, times(1)).onChildProcessDied(connection);
}
@Test
public void testNotBoundToCaller() throws RemoteException {
ChildProcessConnection connection = createTestConnection(false /* bindToCaller */,
false /* bindAsExternalService */, null /* serviceBundle */);
assertNotNull(mFirstServiceConnection);
connection.start(false /* useStrongBinding */, mServiceCallback);
mFirstServiceConnection.notifyServiceConnected(mChildProcessServiceBinder);
// Service is started and bindToCallback is not called.
verify(mServiceCallback, times(1)).onChildStarted();
verify(mServiceCallback, never()).onChildStartFailed(any());
verify(mServiceCallback, never()).onChildProcessDied(connection);
verify(mIChildProcessService, never()).bindToCaller();
}
@Test
public void testBoundToCallerSuccess() throws RemoteException {
ChildProcessConnection connection = createTestConnection(true /* bindToCaller */,
false /* bindAsExternalService */, null /* serviceBundle */);
assertNotNull(mFirstServiceConnection);
connection.start(false /* useStrongBinding */, mServiceCallback);
when(mIChildProcessService.bindToCaller()).thenReturn(true);
mFirstServiceConnection.notifyServiceConnected(mChildProcessServiceBinder);
// Service is started and bindToCallback is called.
verify(mServiceCallback, times(1)).onChildStarted();
verify(mServiceCallback, never()).onChildStartFailed(any());
verify(mServiceCallback, never()).onChildProcessDied(connection);
verify(mIChildProcessService, times(1)).bindToCaller();
}
@Test
public void testBoundToCallerFailure() throws RemoteException {
ChildProcessConnection connection = createTestConnection(true /* bindToCaller */,
false /* bindAsExternalService */, null /* serviceBundle */);
assertNotNull(mFirstServiceConnection);
connection.start(false /* useStrongBinding */, mServiceCallback);
// Pretend bindToCaller returns false, i.e. the service is already bound to a different
// service.
when(mIChildProcessService.bindToCaller()).thenReturn(false);
mFirstServiceConnection.notifyServiceConnected(mChildProcessServiceBinder);
// Service fails to start.
verify(mServiceCallback, never()).onChildStarted();
verify(mServiceCallback, times(1)).onChildStartFailed(any());
verify(mServiceCallback, never()).onChildProcessDied(connection);
verify(mIChildProcessService, times(1)).bindToCaller();
}
@Test
public void testSetupConnectionBeforeServiceConnected() throws RemoteException {
ChildProcessConnection connection = createDefaultTestConnection();
assertNotNull(mFirstServiceConnection);
connection.start(false /* useStrongBinding */, null /* serviceCallback */);
connection.setupConnection(
null /* connectionBundle */, null /* callback */, mConnectionCallback);
verify(mConnectionCallback, never()).onConnected(any());
mFirstServiceConnection.notifyServiceConnected(mChildProcessServiceBinder);
ShadowLooper.runUiThreadTasks();
assertNotNull(mConnectionPidCallback);
mConnectionPidCallback.call(34 /* pid */);
verify(mConnectionCallback, times(1)).onConnected(connection);
}
@Test
public void testSetupConnectionAfterServiceConnected() throws RemoteException {
ChildProcessConnection connection = createDefaultTestConnection();
assertNotNull(mFirstServiceConnection);
connection.start(false /* useStrongBinding */, null /* serviceCallback */);
mFirstServiceConnection.notifyServiceConnected(mChildProcessServiceBinder);
connection.setupConnection(
null /* connectionBundle */, null /* callback */, mConnectionCallback);
verify(mConnectionCallback, never()).onConnected(any());
ShadowLooper.runUiThreadTasks();
assertNotNull(mConnectionPidCallback);
mConnectionPidCallback.call(34 /* pid */);
verify(mConnectionCallback, times(1)).onConnected(connection);
}
@Test
public void testKill() throws RemoteException {
ChildProcessConnection connection = createDefaultTestConnection();
assertNotNull(mFirstServiceConnection);
connection.start(false /* useStrongBinding */, null /* serviceCallback */);
mFirstServiceConnection.notifyServiceConnected(mChildProcessServiceBinder);
connection.setupConnection(
null /* connectionBundle */, null /* callback */, mConnectionCallback);
verify(mConnectionCallback, never()).onConnected(any());
ShadowLooper.runUiThreadTasks();
assertNotNull(mConnectionPidCallback);
mConnectionPidCallback.call(34 /* pid */);
verify(mConnectionCallback, times(1)).onConnected(connection);
// Add strong binding so that connection is oom protected.
connection.removeModerateBinding();
assertEquals(ChildBindingState.WAIVED, connection.bindingStateCurrentOrWhenDied());
connection.addModerateBinding();
assertEquals(ChildBindingState.MODERATE, connection.bindingStateCurrentOrWhenDied());
connection.addStrongBinding();
assertEquals(ChildBindingState.STRONG, connection.bindingStateCurrentOrWhenDied());
// Kill and verify state.
connection.kill();
verify(mIChildProcessService).forceKill();
assertEquals(ChildBindingState.STRONG, connection.bindingStateCurrentOrWhenDied());
Assert.assertTrue(connection.isKilledByUs());
}
@Test
public void testBindingStateCounts() throws RemoteException {
ChildProcessConnection.resetBindingStateCountsForTesting();
ChildProcessConnection connection0 = createDefaultTestConnection();
ChildServiceConnectionMock connectionMock0 = mFirstServiceConnection;
mFirstServiceConnection = null;
ChildProcessConnection connection1 = createDefaultTestConnection();
ChildServiceConnectionMock connectionMock1 = mFirstServiceConnection;
mFirstServiceConnection = null;
ChildProcessConnection connection2 = createDefaultTestConnection();
ChildServiceConnectionMock connectionMock2 = mFirstServiceConnection;
mFirstServiceConnection = null;
assertArrayEquals(
connection0.remainingBindingStateCountsCurrentOrWhenDied(), new int[] {0, 0, 0, 0});
connection0.start(false /* useStrongBinding */, null /* serviceCallback */);
assertArrayEquals(
connection0.remainingBindingStateCountsCurrentOrWhenDied(), new int[] {0, 0, 0, 0});
connection1.start(true /* useStrongBinding */, null /* serviceCallback */);
assertArrayEquals(
connection0.remainingBindingStateCountsCurrentOrWhenDied(), new int[] {0, 0, 0, 1});
connection2.start(false /* useStrongBinding */, null /* serviceCallback */);
assertArrayEquals(
connection0.remainingBindingStateCountsCurrentOrWhenDied(), new int[] {0, 0, 1, 1});
Binder binder0 = new Binder();
Binder binder1 = new Binder();
Binder binder2 = new Binder();
binder0.attachInterface(mIChildProcessService, IChildProcessService.class.getName());
binder1.attachInterface(mIChildProcessService, IChildProcessService.class.getName());
binder2.attachInterface(mIChildProcessService, IChildProcessService.class.getName());
connectionMock0.notifyServiceConnected(binder0);
connectionMock1.notifyServiceConnected(binder1);
connectionMock2.notifyServiceConnected(binder2);
ShadowLooper.runUiThreadTasks();
// Add and remove moderate binding works as expected.
connection2.removeModerateBinding();
assertArrayEquals(
connection0.remainingBindingStateCountsCurrentOrWhenDied(), new int[] {0, 1, 0, 1});
connection2.addModerateBinding();
assertArrayEquals(
connection0.remainingBindingStateCountsCurrentOrWhenDied(), new int[] {0, 0, 1, 1});
// Add and remove strong binding works as expected.
connection0.addStrongBinding();
assertArrayEquals(
connection0.remainingBindingStateCountsCurrentOrWhenDied(), new int[] {0, 0, 1, 1});
connection0.removeStrongBinding();
assertArrayEquals(
connection0.remainingBindingStateCountsCurrentOrWhenDied(), new int[] {0, 0, 1, 1});
// Stopped connection should no longe update.
connection0.stop();
assertArrayEquals(
connection0.remainingBindingStateCountsCurrentOrWhenDied(), new int[] {0, 0, 1, 1});
assertArrayEquals(
connection1.remainingBindingStateCountsCurrentOrWhenDied(), new int[] {0, 0, 1, 0});
connection2.removeModerateBinding();
assertArrayEquals(
connection0.remainingBindingStateCountsCurrentOrWhenDied(), new int[] {0, 0, 1, 1});
assertArrayEquals(
connection1.remainingBindingStateCountsCurrentOrWhenDied(), new int[] {0, 1, 0, 0});
}
}