// Copyright 2014 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.content.browser;
import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL;
import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE;
import android.app.Activity;
import android.app.Application;
import android.content.ComponentName;
import android.os.Build;
import android.util.Pair;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;
import org.robolectric.shadows.ShadowLooper;
import org.chromium.base.FeatureList;
import org.chromium.base.process_launcher.ChildProcessConnection;
import org.chromium.base.process_launcher.TestChildProcessConnection;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Feature;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Unit tests for BindingManager and ChildProcessConnection.
*
* Default property of being low-end device is overriden, so that both low-end and high-end policies
* are tested.
*/
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE, sdk = Build.VERSION_CODES.Q)
@LooperMode(LooperMode.Mode.LEGACY)
public class BindingManagerTest {
private static final int BINDING_COUNT_LIMIT = 5;
// Creates a mocked ChildProcessConnection that is optionally added to a BindingManager.
private static ChildProcessConnection createTestChildProcessConnection(
int pid, BindingManager manager, List<ChildProcessConnection> iterable) {
TestChildProcessConnection connection =
new TestChildProcessConnection(
new ComponentName("org.chromium.test", "TestService"),
/* bindToCallerCheck= */ false,
/* bindAsExternalService= */ false,
/* serviceBundle= */ null);
connection.setPid(pid);
connection.start(/* useStrongBinding= */ false, /* serviceCallback= */ null);
if (manager != null) {
manager.addConnection(connection);
}
iterable.add(connection);
connection.removeVisibleBinding(); // Remove initial binding.
return connection;
}
Activity mActivity;
// Created in setUp() for convenience.
BindingManager mManager;
BindingManager mVariableManager;
List<ChildProcessConnection> mIterable;
@Before
public void setUp() {
// The tests run on only one thread. Pretend that is the launcher thread so LauncherThread
// asserts are not triggered.
LauncherThread.setCurrentThreadAsLauncherThread();
mActivity = Robolectric.buildActivity(Activity.class).setup().get();
mIterable = new ArrayList<>();
mManager = new BindingManager(mActivity, BINDING_COUNT_LIMIT, mIterable);
mVariableManager = new BindingManager(mActivity, BindingManager.NO_MAX_SIZE, mIterable);
}
@After
public void tearDown() {
LauncherThread.setLauncherThreadAsLauncherThread();
FeatureList.setTestValues(null);
}
private void setupBindingType(boolean useNotPerceptibleBinding) {
boolean isQOrHigher = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
BindingManager.setUseNotPerceptibleBindingForTesting(
useNotPerceptibleBinding && isQOrHigher);
if (useNotPerceptibleBinding) {
Assert.assertEquals(isQOrHigher, BindingManager.useNotPerceptibleBinding());
return;
}
Assert.assertFalse(BindingManager.useNotPerceptibleBinding());
}
private void checkConnections(
ChildProcessConnection[] connections,
boolean useNotPerceptibleBinding,
boolean isConnected) {
boolean[] connected = new boolean[connections.length];
Arrays.fill(connected, isConnected);
checkConnections(connections, useNotPerceptibleBinding, connected);
}
private void checkConnections(
ChildProcessConnection[] connections,
boolean useNotPerceptibleBinding,
boolean[] connected) {
assert connections.length == connected.length;
for (int i = 0; i < connections.length; i++) {
Assert.assertEquals(
!useNotPerceptibleBinding && connected[i],
connections[i].isVisibleBindingBound());
Assert.assertEquals(
useNotPerceptibleBinding && connected[i],
connections[i].isNotPerceptibleBindingBound());
}
}
/**
* Verifies that onSentToBackground() drops all the moderate bindings after some delay, and
* onBroughtToForeground() doesn't recover them.
*/
@Test
@Feature({"ProcessManagement"})
public void testVisibleBindingDropOnBackground() {
setupBindingType(false);
doTestBindingDropOnBackground(mManager);
}
@Test
@Feature({"ProcessManagement"})
public void testVisibleBindingDropOnBackgroundWithVariableSize() {
setupBindingType(false);
doTestBindingDropOnBackground(mVariableManager);
}
@Test
@Feature({"ProcessManagement"})
public void testNotPerceptibleBindingDropOnBackground() {
setupBindingType(true);
doTestBindingDropOnBackground(mManager);
}
@Test
@Feature({"ProcessManagement"})
public void testNotPerceptibleBindingDropOnBackgroundWithVariableSize() {
setupBindingType(true);
doTestBindingDropOnBackground(mVariableManager);
}
private void doTestBindingDropOnBackground(BindingManager manager) {
ChildProcessConnection[] connections = new ChildProcessConnection[3];
for (int i = 0; i < connections.length; i++) {
connections[i] = createTestChildProcessConnection(/* pid= */ i + 1, manager, mIterable);
}
// Verify that each connection has a moderate binding after binding and releasing a strong
// binding.
checkConnections(
connections, BindingManager.useNotPerceptibleBinding(), /* isConnected= */ true);
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
// Verify that leaving the application for a short time doesn't clear the moderate bindings.
manager.onSentToBackground();
checkConnections(
connections, BindingManager.useNotPerceptibleBinding(), /* isConnected= */ true);
manager.onBroughtToForeground();
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
checkConnections(
connections, BindingManager.useNotPerceptibleBinding(), /* isConnected= */ true);
// Call onSentToBackground() and verify that all the moderate bindings drop after some
// delay.
manager.onSentToBackground();
checkConnections(
connections, BindingManager.useNotPerceptibleBinding(), /* isConnected= */ true);
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
checkConnections(
connections, BindingManager.useNotPerceptibleBinding(), /* isConnected= */ false);
// Call onBroughtToForeground() and verify that the previous moderate bindings aren't
// recovered.
manager.onBroughtToForeground();
checkConnections(
connections, BindingManager.useNotPerceptibleBinding(), /* isConnected= */ false);
}
/** Verifies that onLowMemory() drops all the moderate bindings. */
@Test
@Feature({"ProcessManagement"})
public void testVisibleBindingDropOnLowMemory() {
setupBindingType(false);
doTestBindingDropOnLowMemory(mManager);
}
@Test
@Feature({"ProcessManagement"})
public void testVisibleBindingDropOnLowMemoryVariableSize() {
setupBindingType(false);
doTestBindingDropOnLowMemory(mVariableManager);
}
@Test
@Feature({"ProcessManagement"})
public void testNotPerceptibleBindingDropOnLowMemory() {
setupBindingType(true);
doTestBindingDropOnLowMemory(mManager);
}
@Test
@Feature({"ProcessManagement"})
public void testNotPerceptibleBindingDropOnLowMemoryVariableSize() {
setupBindingType(true);
doTestBindingDropOnLowMemory(mVariableManager);
}
private void doTestBindingDropOnLowMemory(BindingManager manager) {
final Application app = mActivity.getApplication();
ChildProcessConnection[] connections = new ChildProcessConnection[4];
for (int i = 0; i < connections.length; i++) {
connections[i] = createTestChildProcessConnection(/* pid= */ i + 1, manager, mIterable);
}
checkConnections(
connections, BindingManager.useNotPerceptibleBinding(), /* isConnected= */ true);
// Call onLowMemory() and verify that all the moderate bindings drop.
app.onLowMemory();
checkConnections(
connections, BindingManager.useNotPerceptibleBinding(), /* isConnected= */ false);
}
/** Verifies that onTrimMemory() drops moderate bindings properly. */
@Test
@Feature({"ProcessManagement"})
public void testVisibleBindingDropOnTrimMemory() {
setupBindingType(false);
doTestBindingDropOnTrimMemory(mManager);
}
@Test
@Feature({"ProcessManagement"})
public void testVisibleBindingDropOnTrimMemoryWithVariableSize() {
setupBindingType(false);
doTestBindingDropOnTrimMemory(mVariableManager);
}
@Test
@Feature({"ProcessManagement"})
public void testNotPerceptibleBindingDropOnTrimMemory() {
setupBindingType(true);
doTestBindingDropOnTrimMemory(mManager);
}
@Test
@Feature({"ProcessManagement"})
public void testNotPerceptibleBindingDropOnTrimMemoryWithVariableSize() {
setupBindingType(true);
doTestBindingDropOnTrimMemory(mVariableManager);
}
private void doTestBindingDropOnTrimMemory(BindingManager manager) {
final Application app = mActivity.getApplication();
// This test applies only to the moderate-binding manager.
ArrayList<Pair<Integer, Integer>> levelAndExpectedVictimCountList = new ArrayList<>();
levelAndExpectedVictimCountList.add(
new Pair<Integer, Integer>(TRIM_MEMORY_RUNNING_MODERATE, 1));
levelAndExpectedVictimCountList.add(new Pair<Integer, Integer>(TRIM_MEMORY_RUNNING_LOW, 2));
levelAndExpectedVictimCountList.add(
new Pair<Integer, Integer>(TRIM_MEMORY_RUNNING_CRITICAL, 4));
ChildProcessConnection[] connections = new ChildProcessConnection[4];
for (int i = 0; i < connections.length; i++) {
connections[i] = createTestChildProcessConnection(/* pid= */ i + 1, manager, mIterable);
}
for (Pair<Integer, Integer> pair : levelAndExpectedVictimCountList) {
String message = "Failed for the level=" + pair.first;
mIterable.clear();
// Verify that each connection has a moderate binding after binding and releasing a
// strong binding.
for (ChildProcessConnection connection : connections) {
manager.addConnection(connection);
mIterable.add(connection);
}
checkConnections(
connections,
BindingManager.useNotPerceptibleBinding(),
/* isConnected= */ true);
app.onTrimMemory(pair.first);
// Verify that some of the moderate bindings have been dropped.
for (int i = 0; i < connections.length; i++) {
Assert.assertEquals(
message,
i >= pair.second,
BindingManager.useNotPerceptibleBinding()
? connections[i].isNotPerceptibleBindingBound()
: connections[i].isVisibleBindingBound());
}
}
}
/*
* Test that Chrome is sent to the background, that the initially added moderate bindings are
* removed and are not re-added when Chrome is brought back to the foreground.
*/
@Test
@Feature({"ProcessManagement"})
public void testVisibleBindingTillBackgroundedSentToBackground() {
setupBindingType(false);
doTestBindingTillBackgroundedSentToBackground(mManager);
}
@Test
@Feature({"ProcessManagement"})
public void testVisibleBindingTillBackgroundedSentToBackgroundWithVariableSize() {
setupBindingType(false);
doTestBindingTillBackgroundedSentToBackground(mVariableManager);
}
@Test
@Feature({"ProcessManagement"})
public void testNotPerceptibleBindingTillBackgroundedSentToBackground() {
setupBindingType(true);
doTestBindingTillBackgroundedSentToBackground(mManager);
}
@Test
@Feature({"ProcessManagement"})
public void testNotPerceptibleBindingTillBackgroundedSentToBackgroundWithVariableSize() {
setupBindingType(true);
doTestBindingTillBackgroundedSentToBackground(mVariableManager);
}
private void doTestBindingTillBackgroundedSentToBackground(BindingManager manager) {
ChildProcessConnection[] connection = new ChildProcessConnection[1];
connection[0] = createTestChildProcessConnection(0, manager, mIterable);
checkConnections(
connection, BindingManager.useNotPerceptibleBinding(), /* isConnected= */ true);
manager.onSentToBackground();
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
checkConnections(
connection, BindingManager.useNotPerceptibleBinding(), /* isConnected= */ false);
// Bringing Chrome to the foreground should not re-add the moderate bindings.
manager.onBroughtToForeground();
checkConnections(
connection, BindingManager.useNotPerceptibleBinding(), /* isConnected= */ false);
}
@Test
@Feature({"ProcessManagement"})
public void testOneWaivedConnection_VisibleBinding() {
setupBindingType(false);
doTestOneWaivedConnection(mManager);
}
@Test
@Feature({"ProcessManagement"})
public void testOneWaivedConnectionWithVariableSize_VisibleBinding() {
setupBindingType(false);
doTestOneWaivedConnection(mVariableManager);
}
@Test
@Feature({"ProcessManagement"})
public void testOneWaivedConnection_NotPerceptibleBinding() {
setupBindingType(true);
doTestOneWaivedConnection(mManager);
}
@Test
@Feature({"ProcessManagement"})
public void testOneWaivedConnectionWithVariableSize_NotPerceptibleBinding() {
setupBindingType(true);
doTestOneWaivedConnection(mVariableManager);
}
private void doTestOneWaivedConnection(BindingManager manager) {
ChildProcessConnection[] connections = new ChildProcessConnection[3];
for (int i = 0; i < connections.length; i++) {
connections[i] = createTestChildProcessConnection(/* pid= */ i + 1, manager, mIterable);
}
// Make sure binding is added for all connections.
checkConnections(
connections, BindingManager.useNotPerceptibleBinding(), /* isConnected= */ true);
manager.rankingChanged();
checkConnections(
connections,
BindingManager.useNotPerceptibleBinding(),
new boolean[] {false, true, true});
// Move middle connection to be the first (ie lowest ranked).
mIterable.set(0, connections[1]);
mIterable.set(1, connections[0]);
manager.rankingChanged();
checkConnections(
connections,
BindingManager.useNotPerceptibleBinding(),
new boolean[] {true, false, true});
// Swap back.
mIterable.set(0, connections[0]);
mIterable.set(1, connections[1]);
manager.rankingChanged();
checkConnections(
connections,
BindingManager.useNotPerceptibleBinding(),
new boolean[] {false, true, true});
manager.removeConnection(connections[1]);
checkConnections(
connections,
BindingManager.useNotPerceptibleBinding(),
new boolean[] {false, false, true});
manager.removeConnection(connections[0]);
checkConnections(
connections,
BindingManager.useNotPerceptibleBinding(),
new boolean[] {false, false, true});
}
@Test
@Feature({"ProcessManagement"})
public void testBindingCountLimit_VisibleBinding() {
setupBindingType(false);
doTestBindingCountLimit(mManager, /* limited= */ true);
}
@Test
@Feature({"ProcessManagement"})
public void testNoBindingCountLimitWithVariableSize_VisibleBinding() {
setupBindingType(false);
doTestBindingCountLimit(mVariableManager, /* limited= */ false);
}
@Test
@Feature({"ProcessManagement"})
public void testBindingCountLimit_NotPerceptibleBinding() {
setupBindingType(true);
doTestBindingCountLimit(mManager, /* limited= */ true);
}
@Test
@Feature({"ProcessManagement"})
public void testNoBindingCountLimitWithVariableSize_NotPerceptibleBinding() {
setupBindingType(true);
doTestBindingCountLimit(mVariableManager, /* limited= */ false);
}
private void doTestBindingCountLimit(BindingManager manager, boolean limited) {
ChildProcessConnection[] connections = new ChildProcessConnection[BINDING_COUNT_LIMIT + 1];
for (int i = 0; i < connections.length; i++) {
connections[i] = createTestChildProcessConnection(/* pid= */ i + 1, manager, mIterable);
}
if (!limited) {
checkConnections(
connections,
BindingManager.useNotPerceptibleBinding(),
/* isConnected= */ true);
} else {
checkConnections(
connections,
BindingManager.useNotPerceptibleBinding(),
new boolean[] {false, true, true, true, true, true});
}
}
@Test
@Feature({"ProcessManagement"})
public void testBindingCountLimitLowestRankAddedLast_VisibleBinding() {
setupBindingType(false);
doTestBindingCountLimitLowestRankAddedLast(mManager, /* limited= */ true);
}
@Test
@Feature({"ProcessManagement"})
public void testNoBindingCountLimitLowestRankAddedLastWithVariableSize_VisibleBinding() {
setupBindingType(false);
doTestBindingCountLimitLowestRankAddedLast(mVariableManager, /* limited= */ false);
}
@Test
@Feature({"ProcessManagement"})
public void testBindingCountLimitLowestRankAddedLast_NotPerceptibleBinding() {
setupBindingType(true);
doTestBindingCountLimitLowestRankAddedLast(mManager, /* limited= */ true);
}
@Test
@Feature({"ProcessManagement"})
public void testNoBindingCountLimitLowestRankAddedLastWithVariableSize_NotPerceptibleBinding() {
setupBindingType(true);
doTestBindingCountLimitLowestRankAddedLast(mVariableManager, /* limited= */ false);
}
private void doTestBindingCountLimitLowestRankAddedLast(
BindingManager manager, boolean limited) {
ChildProcessConnection[] connections = new ChildProcessConnection[BINDING_COUNT_LIMIT + 1];
for (int i = 0; i < connections.length; i++) {
connections[i] = createTestChildProcessConnection(/* pid= */ i + 1, null, mIterable);
}
// Add the lowest ranked connection last to ensure it doesn't get added if the limit is
// applied.
mIterable.set(0, connections[BINDING_COUNT_LIMIT]);
mIterable.set(BINDING_COUNT_LIMIT, connections[0]);
for (int i = 0; i < connections.length; i++) {
manager.addConnection(connections[i]);
}
if (!limited) {
checkConnections(
connections,
BindingManager.useNotPerceptibleBinding(),
/* isConnected= */ true);
} else {
checkConnections(
connections,
BindingManager.useNotPerceptibleBinding(),
new boolean[] {true, true, true, true, true, false});
}
}
}