| // 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); |
| } |
| } |