blob: efa5df228b06065f875032590699b7556ed73429 [file] [log] [blame]
// Copyright 2015 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.chrome.browser;
import android.content.Context;
import android.support.test.filters.LargeTest;
import android.test.MoreAsserts;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Restriction;
import org.chromium.base.test.util.RetryOnFailure;
import org.chromium.chrome.browser.compositor.layouts.Layout;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabCreatorManager.TabCreator;
import org.chromium.chrome.browser.tabmodel.TabModel.TabLaunchType;
import org.chromium.chrome.browser.tabmodel.TabModel.TabSelectionType;
import org.chromium.chrome.browser.tabmodel.TabModelUtils;
import org.chromium.chrome.test.ChromeActivityTestCaseBase;
import org.chromium.chrome.test.util.ChromeRestriction;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.chrome.test.util.PrerenderTestHelper;
import org.chromium.content.browser.BindingManager;
import org.chromium.content.browser.ChildProcessConnection;
import org.chromium.content.browser.ChildProcessLauncher;
import org.chromium.content.browser.test.ChildProcessAllocatorSettings;
import org.chromium.content.browser.test.util.Criteria;
import org.chromium.content.browser.test.util.CriteriaHelper;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.net.test.EmbeddedTestServer;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.base.PageTransition;
import java.util.concurrent.Callable;
/**
* Integration tests for the BindingManager API. This test plants a mock BindingManager
* implementation and verifies that the signals it relies on are correctly delivered.
*/
@RetryOnFailure
public class BindingManagerIntegrationTest extends ChromeActivityTestCaseBase<ChromeActivity> {
private static class MockBindingManager implements BindingManager {
// Maps pid to the last received visibility state of the renderer.
private final SparseBooleanArray mProcessInForegroundMap = new SparseBooleanArray();
// Maps pid to a string recording calls to setInForeground() and visibilityDetermined().
private final SparseArray<String> mVisibilityCallsMap = new SparseArray<String>();
private boolean mIsReleaseAllModerateBindingsCalled;
void assertIsInForeground(final int pid) {
CriteriaHelper.pollInstrumentationThread(new Criteria() {
@Override
public boolean isSatisfied() {
return mProcessInForegroundMap.get(pid);
}
});
}
void assertIsInBackground(final int pid) {
CriteriaHelper.pollInstrumentationThread(new Criteria() {
@Override
public boolean isSatisfied() {
return !mProcessInForegroundMap.get(pid);
}
});
}
void assertSetInForegroundWasCalled(String message, final int pid) {
CriteriaHelper.pollInstrumentationThread(new Criteria(message) {
@Override
public boolean isSatisfied() {
return mProcessInForegroundMap.indexOfKey(pid) >= 0;
}
});
}
void assertIsReleaseAllModerateBindingsCalled() {
CriteriaHelper.pollInstrumentationThread(new Criteria() {
@Override
public boolean isSatisfied() {
return mIsReleaseAllModerateBindingsCalled;
}
});
}
String getVisibilityCalls(int pid) {
synchronized (mVisibilityCallsMap) {
return mVisibilityCallsMap.get(pid);
}
}
boolean isReleaseAllModerateBindingsCalled() {
return mIsReleaseAllModerateBindingsCalled;
}
@Override
public void addNewConnection(int pid, ChildProcessConnection connection) {
synchronized (mVisibilityCallsMap) {
mVisibilityCallsMap.put(pid, "");
}
}
@Override
public void setInForeground(int pid, boolean inForeground) {
mProcessInForegroundMap.put(pid, inForeground);
synchronized (mVisibilityCallsMap) {
if (inForeground) {
mVisibilityCallsMap.put(pid, mVisibilityCallsMap.get(pid) + "FG;");
} else {
mVisibilityCallsMap.put(pid, mVisibilityCallsMap.get(pid) + "BG;");
}
}
}
@Override
public void determinedVisibility(int pid) {
synchronized (mVisibilityCallsMap) {
mVisibilityCallsMap.put(pid, mVisibilityCallsMap.get(pid) + "DETERMINED;");
}
}
@Override
public void onSentToBackground() {}
@Override
public void onBroughtToForeground() {}
@Override
public boolean isOomProtected(int pid) {
return false;
}
@Override
public void clearConnection(int pid) {}
@Override
public void startModerateBindingManagement(Context context, int maxSize) {}
@Override
public void releaseAllModerateBindings() {
mIsReleaseAllModerateBindingsCalled = true;
}
}
private MockBindingManager mBindingManager;
private EmbeddedTestServer mTestServer;
private static final String FILE_PATH = "/chrome/test/data/android/test.html";
private static final String FILE_PATH2 = "/chrome/test/data/android/simple.html";
// about:version will always be handled by a different renderer than a local file.
private static final String ABOUT_VERSION_PATH = "chrome://version/";
private static final String SHARED_RENDERER_PAGE_PATH =
"/chrome/test/data/android/bindingmanager/shared_renderer1.html";
private static final String SHARED_RENDERER_PAGE2_PATH =
"/chrome/test/data/android/bindingmanager/shared_renderer2.html";
public BindingManagerIntegrationTest() {
super(ChromeActivity.class);
}
/**
* Verifies that the .setProcessInForeground() signal is called correctly as the tabs are
* created and switched.
*/
@LargeTest
@Feature({"ProcessManagement"})
public void testTabSwitching() throws InterruptedException {
// Create two tabs and wait until they are loaded, so that their renderers are around.
final Tab[] tabs = new Tab[2];
getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
// Foreground tab.
TabCreator tabCreator = getActivity().getCurrentTabCreator();
tabs[0] = tabCreator.createNewTab(
new LoadUrlParams(mTestServer.getURL(FILE_PATH)),
TabLaunchType.FROM_CHROME_UI, null);
// Background tab.
tabs[1] = tabCreator.createNewTab(
new LoadUrlParams(mTestServer.getURL(FILE_PATH)),
TabLaunchType.FROM_LONGPRESS_BACKGROUND, null);
// On Svelte devices the background tab would not be loaded automatically, so
// trigger the load manually.
tabs[1].show(TabSelectionType.FROM_USER);
tabs[1].hide();
}
});
ChromeTabUtils.waitForTabPageLoaded(tabs[0], mTestServer.getURL(FILE_PATH));
ChromeTabUtils.waitForTabPageLoaded(tabs[1], mTestServer.getURL(FILE_PATH));
// Wait for the new tab animations on phones to finish.
if (!DeviceFormFactor.isTablet(getActivity())) {
final ChromeActivity activity = getActivity();
CriteriaHelper.pollUiThread(new Criteria("Did not finish animation") {
@Override
public boolean isSatisfied() {
Layout layout = activity.getCompositorViewHolder()
.getLayoutManager().getActiveLayout();
return !layout.isLayoutAnimating();
}
});
}
getInstrumentation().waitForIdleSync();
getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
// Make sure that the renderers were spawned.
assertTrue(tabs[0].getContentViewCore().getCurrentRenderProcessId() > 0);
assertTrue(tabs[1].getContentViewCore().getCurrentRenderProcessId() > 0);
// Verify that the renderer of the foreground tab was signalled as visible.
mBindingManager.assertIsInForeground(
tabs[0].getContentViewCore().getCurrentRenderProcessId());
// Verify that the renderer of the tab loaded in background was signalled as not
// visible.
mBindingManager.assertIsInBackground(
tabs[1].getContentViewCore().getCurrentRenderProcessId());
// Select tabs[1] and verify that the renderer visibility was flipped.
TabModelUtils.setIndex(getActivity().getCurrentTabModel(), indexOf(tabs[1]));
mBindingManager.assertIsInBackground(
tabs[0].getContentViewCore().getCurrentRenderProcessId());
mBindingManager.assertIsInForeground(
tabs[1].getContentViewCore().getCurrentRenderProcessId());
}
});
}
/**
* Verifies that the .setProcessInForeground() signal is called correctly when a tab that
* crashed in background is restored in foreground. This is a regression test for
* http://crbug.com/399521.
*/
@DisabledTest(message = "crbug.com/543153")
@LargeTest
@Feature({"ProcessManagement"})
public void testCrashInBackground() throws InterruptedException {
// Create two tabs and wait until they are loaded, so that their renderers are around.
final Tab[] tabs = new Tab[2];
getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
// Foreground tab.
TabCreator tabCreator = getActivity().getCurrentTabCreator();
tabs[0] = tabCreator.createNewTab(
new LoadUrlParams(mTestServer.getURL(FILE_PATH)),
TabLaunchType.FROM_CHROME_UI, null);
// Background tab.
tabs[1] = tabCreator.createNewTab(
new LoadUrlParams(mTestServer.getURL(FILE_PATH)),
TabLaunchType.FROM_LONGPRESS_BACKGROUND, null);
// On Svelte devices the background tab would not be loaded automatically, so
// trigger the load manually.
tabs[1].show(TabSelectionType.FROM_USER);
tabs[1].hide();
}
});
ChromeTabUtils.waitForTabPageLoaded(tabs[0], mTestServer.getURL(FILE_PATH));
ChromeTabUtils.waitForTabPageLoaded(tabs[1], mTestServer.getURL(FILE_PATH));
// Wait for the new tab animations on phones to finish.
if (!DeviceFormFactor.isTablet(getActivity())) {
final ChromeActivity activity = getActivity();
CriteriaHelper.pollUiThread(new Criteria("Did not finish animation") {
@Override
public boolean isSatisfied() {
Layout layout = activity.getCompositorViewHolder()
.getLayoutManager().getActiveLayout();
return !layout.isLayoutAnimating();
}
});
}
getInstrumentation().waitForIdleSync();
getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
// Make sure that the renderers were spawned.
assertTrue(tabs[0].getContentViewCore().getCurrentRenderProcessId() > 0);
assertTrue(tabs[1].getContentViewCore().getCurrentRenderProcessId() > 0);
// Verify that the renderer of the foreground tab was signalled as visible.
mBindingManager.assertIsInForeground(
tabs[0].getContentViewCore().getCurrentRenderProcessId());
// Verify that the renderer of the tab loaded in background was signalled as not
// visible.
mBindingManager.assertIsInBackground(
tabs[1].getContentViewCore().getCurrentRenderProcessId());
}
});
// Kill the renderer and wait for the crash to be noted by the browser process.
assertTrue(ChildProcessLauncher.crashProcessForTesting(
tabs[1].getContentViewCore().getCurrentRenderProcessId()));
CriteriaHelper.pollInstrumentationThread(
new Criteria("Renderer crash wasn't noticed by the browser.") {
@Override
public boolean isSatisfied() {
return tabs[1].getContentViewCore().getCurrentRenderProcessId() == 0;
}
});
// Switch to the tab that crashed in background.
getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
TabModelUtils.setIndex(getActivity().getCurrentTabModel(), indexOf(tabs[1]));
}
});
// Wait until the process is spawned and its visibility is determined.
CriteriaHelper.pollInstrumentationThread(
new Criteria("Process for the crashed tab was not respawned.") {
@Override
public boolean isSatisfied() {
return tabs[1].getContentViewCore().getCurrentRenderProcessId() != 0;
}
});
mBindingManager.assertSetInForegroundWasCalled(
"isInForeground() was not called for the process.",
tabs[1].getContentViewCore().getCurrentRenderProcessId());
getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
// Verify the visibility of the renderers.
mBindingManager.assertIsInBackground(
tabs[0].getContentViewCore().getCurrentRenderProcessId());
mBindingManager.assertIsInForeground(
tabs[1].getContentViewCore().getCurrentRenderProcessId());
}
});
}
/**
* Verifies that a renderer that crashes in foreground has the correct visibility when
* recreated.
*/
@LargeTest
@Feature({"ProcessManagement"})
public void testCrashInForeground() throws InterruptedException {
// Create a tab in foreground and wait until it is loaded.
final String testUrl = mTestServer.getURL(FILE_PATH);
final Tab tab = ThreadUtils.runOnUiThreadBlockingNoException(
new Callable<Tab>() {
@Override
public Tab call() throws Exception {
TabCreator tabCreator = getActivity().getCurrentTabCreator();
return tabCreator.createNewTab(
new LoadUrlParams(testUrl), TabLaunchType.FROM_CHROME_UI, null);
}
});
ChromeTabUtils.waitForTabPageLoaded(tab, testUrl);
getInstrumentation().waitForIdleSync();
// Kill the renderer and wait for the crash to be noted by the browser process.
assertTrue(ChildProcessLauncher.crashProcessForTesting(
tab.getContentViewCore().getCurrentRenderProcessId()));
CriteriaHelper.pollInstrumentationThread(
new Criteria("Renderer crash wasn't noticed by the browser.") {
@Override
public boolean isSatisfied() {
return tab.getContentViewCore().getCurrentRenderProcessId() == 0;
}
});
// Reload the tab, respawning the renderer.
getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
tab.reload();
}
});
ChromeTabUtils.waitForTabPageLoaded(tab, testUrl);
// Wait until the process is spawned and its visibility is determined.
CriteriaHelper.pollInstrumentationThread(
new Criteria("Process for the crashed tab was not respawned.") {
@Override
public boolean isSatisfied() {
return tab.getContentViewCore().getCurrentRenderProcessId() != 0;
}
});
mBindingManager.assertSetInForegroundWasCalled(
"isInForeground() was not called for the process.",
tab.getContentViewCore().getCurrentRenderProcessId());
getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
// Verify the visibility of the renderer.
mBindingManager.assertIsInForeground(
tab.getContentViewCore().getCurrentRenderProcessId());
}
});
}
private int getRenderProcessId(final Tab tab) {
return ThreadUtils.runOnUiThreadBlockingNoException(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return tab.getContentViewCore().getCurrentRenderProcessId();
}
});
}
/**
* Ensures correctness of the visibilityDetermined() calls, that would be generally preceded by
* setInForeground(), but it can't be guaranteed because they are triggered from different
* threads.
*/
@LargeTest
@Feature({"ProcessManagement"})
public void testVisibilityDetermined() throws InterruptedException {
// Create a tab in foreground and wait until it is loaded.
final Tab fgTab = ThreadUtils.runOnUiThreadBlockingNoException(
new Callable<Tab>() {
@Override
public Tab call() {
TabCreator tabCreator = getActivity().getCurrentTabCreator();
return tabCreator.createNewTab(
new LoadUrlParams(mTestServer.getURL(FILE_PATH)),
TabLaunchType.FROM_CHROME_UI, null);
}});
ChromeTabUtils.waitForTabPageLoaded(fgTab, mTestServer.getURL(FILE_PATH));
int initialNavigationPid = getRenderProcessId(fgTab);
// Ensure the following calls happened:
// - FG - setInForeground(true) - when the tab is created in the foreground
// - DETERMINED - visibilityDetermined() - after the initial navigation is committed
assertEquals("FG;DETERMINED;", mBindingManager.getVisibilityCalls(initialNavigationPid));
// Navigate to about:version which requires a different renderer.
loadUrlInTab(ABOUT_VERSION_PATH, PageTransition.LINK, fgTab);
int secondNavigationPid = getRenderProcessId(fgTab);
MoreAsserts.assertNotEqual(secondNavigationPid, initialNavigationPid);
// Ensure the following calls happened:
// - BG - setInForeground(false) - when the renderer is created for uncommited frame
// - FG - setInForeground(true) - when the frame is swapped in on commit
// - DETERMINED - visibilityDetermined() - after the navigation is committed
// Or BG -> DETERMINED -> FG is also possible because setInForeground() and
// visibilityDetermined() are triggered from different threads.
mBindingManager.assertIsInForeground(secondNavigationPid);
String visibilityCalls = mBindingManager.getVisibilityCalls(secondNavigationPid);
assertTrue(visibilityCalls, "BG;FG;DETERMINED;".equals(visibilityCalls)
|| "BG;DETERMINED;FG;".equals(visibilityCalls));
// Open a tab in the background and load it.
final Tab bgTab = ThreadUtils.runOnUiThreadBlockingNoException(
new Callable<Tab>() {
@Override
public Tab call() {
TabCreator tabCreator = getActivity().getCurrentTabCreator();
Tab tab = tabCreator.createNewTab(
new LoadUrlParams(mTestServer.getURL(FILE_PATH)),
TabLaunchType.FROM_LONGPRESS_BACKGROUND, null);
// On Svelte devices the background tab would not be loaded automatically,
// so trigger the load manually.
tab.show(TabSelectionType.FROM_USER);
tab.hide();
return tab;
}});
ChromeTabUtils.waitForTabPageLoaded(bgTab, mTestServer.getURL(FILE_PATH));
int bgNavigationPid = getRenderProcessId(bgTab);
// Ensure the following calls happened:
// - BG - setInForeground(false) - when tab is created in the background
// - DETERMINED - visibilityDetermined() - after the navigation is committed
assertEquals("BG;DETERMINED;", mBindingManager.getVisibilityCalls(bgNavigationPid));
}
/**
* When a user navigates to a prererendered URL, the tab swaps in the prerender's render
* process and discards its old render process. Test that visibilityDetermined() is called for
* the swapped in render process.
*/
@LargeTest
@Restriction({Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE})
@Feature({"ProcessManagement"})
public void testVisibilityDeterminedNavigateToPrerenderedPage() throws InterruptedException {
loadUrl(mTestServer.getURL(FILE_PATH));
Tab tab = getActivity().getActivityTab();
int pid1 = getRenderProcessId(tab);
String prerenderUrl = mTestServer.getURL(FILE_PATH2);
PrerenderTestHelper.prerenderUrl(prerenderUrl, tab);
assertEquals(TabLoadStatus.FULL_PRERENDERED_PAGE_LOAD, loadUrl(prerenderUrl));
int pid2 = getRenderProcessId(tab);
MoreAsserts.assertNotEqual(pid1, pid2);
assertTrue(mBindingManager.getVisibilityCalls(pid1).contains("DETERMINED;"));
assertTrue(mBindingManager.getVisibilityCalls(pid2).contains("DETERMINED;"));
}
/**
* Verifies that BindingManager.releaseAllModerateBindings() is called once all the sandboxed
* services are allocated.
*/
@ChildProcessAllocatorSettings(sandboxedServiceCount = 4)
@LargeTest
@Feature({"ProcessManagement"})
public void testReleaseAllModerateBindings() throws InterruptedException {
final TabCreator tabCreator = getActivity().getCurrentTabCreator();
final Tab[] tabs = new Tab[3];
getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
// Foreground tab.
tabs[0] = tabCreator.createNewTab(
new LoadUrlParams("about:blank"), TabLaunchType.FROM_CHROME_UI, null);
// Background tab.
tabs[1] = tabCreator.createNewTab(
new LoadUrlParams("about:blank"), TabLaunchType.FROM_CHROME_UI, null);
}
});
ChromeTabUtils.waitForTabPageLoaded(tabs[0], "about:blank");
ChromeTabUtils.waitForTabPageLoaded(tabs[1], "about:blank");
// At this point 3 sanboxed services are allocated; the initial one + 2 new tabs.
assertFalse(mBindingManager.isReleaseAllModerateBindingsCalled());
getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
// Foreground tab.
tabs[2] = tabCreator.createNewTab(
new LoadUrlParams("about:blank"), TabLaunchType.FROM_CHROME_UI, null);
}
});
ChromeTabUtils.waitForTabPageLoaded(tabs[2], "about:blank");
// At this point all the sanboxed services are allocated.
mBindingManager.assertIsReleaseAllModerateBindingsCalled();
}
// Test crashes on tablets. See crbug.com/594407
@LargeTest
@Feature({"ProcessManagement"})
@Restriction(ChromeRestriction.RESTRICTION_TYPE_PHONE)
public void testRestoreSharedRenderer() throws Exception {
loadUrl(mTestServer.getURL(SHARED_RENDERER_PAGE_PATH));
final Tab[] tabs = new Tab[2];
tabs[0] = getActivity().getActivityTab();
singleClickView(tabs[0].getView());
CriteriaHelper.pollInstrumentationThread(new Criteria("Child tab isn't opened.") {
@Override
public boolean isSatisfied() {
return getActivity().getCurrentTabModel().getCount() == 2
&& tabs[0] != getActivity().getActivityTab()
&& getActivity()
.getActivityTab()
.getContentViewCore()
.getCurrentRenderProcessId()
!= 0;
}
});
tabs[1] = getActivity().getActivityTab();
assertEquals(tabs[0].getContentViewCore().getCurrentRenderProcessId(),
tabs[1].getContentViewCore().getCurrentRenderProcessId());
getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
// Verify the visibility of the renderer.
mBindingManager.assertIsInForeground(
tabs[0].getContentViewCore().getCurrentRenderProcessId());
}
});
assertTrue(ChildProcessLauncher.crashProcessForTesting(
tabs[1].getContentViewCore().getCurrentRenderProcessId()));
CriteriaHelper.pollInstrumentationThread(
new Criteria("Renderer crash wasn't noticed by the browser.") {
@Override
public boolean isSatisfied() {
return tabs[1].getContentViewCore().getCurrentRenderProcessId() == 0;
}
});
// Reload the tab, respawning the renderer.
getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
tabs[1].reload();
}
});
ChromeTabUtils.waitForTabPageLoaded(
tabs[1], mTestServer.getURL(SHARED_RENDERER_PAGE2_PATH));
// Wait until the process is spawned and its visibility is determined.
CriteriaHelper.pollInstrumentationThread(
new Criteria("Process for the crashed tab was not respawned.") {
@Override
public boolean isSatisfied() {
return tabs[1].getContentViewCore().getCurrentRenderProcessId() != 0;
}
});
mBindingManager.assertSetInForegroundWasCalled(
"setInForeground() was not called for the process.",
tabs[1].getContentViewCore().getCurrentRenderProcessId());
getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
// Verify the visibility of the renderer.
mBindingManager.assertIsInForeground(
tabs[1].getContentViewCore().getCurrentRenderProcessId());
tabs[1].hide();
mBindingManager.assertIsInBackground(
tabs[1].getContentViewCore().getCurrentRenderProcessId());
}
});
}
@Override
public void startMainActivity() throws InterruptedException {
startMainActivityOnBlankPage();
}
@Override
protected void setUp() throws Exception {
// Hook in the test binding manager.
mBindingManager = new MockBindingManager();
ChildProcessLauncher.setBindingManagerForTesting(mBindingManager);
super.setUp();
mTestServer = EmbeddedTestServer.createAndStartServer(getInstrumentation().getContext());
}
@Override
protected void tearDown() throws Exception {
mTestServer.stopAndDestroyServer();
super.tearDown();
}
/**
* @return the index of the given tab in the current tab model
*/
private int indexOf(Tab tab) {
return getActivity().getCurrentTabModel().indexOf(tab);
}
}