blob: 05e8bde230082f07bc3bb357132c2ea112212de9 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tests.rollback.host;
import static com.android.tests.rollback.host.WatchdogEventLogger.Subject.assertThat;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assume.assumeTrue;
import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
import java.util.concurrent.TimeUnit;
/**
* Runs the staged rollback tests.
*
* TODO(gavincorkery): Support the verification of logging parents in Watchdog metrics.
*/
@RunWith(DeviceJUnit4ClassRunner.class)
public class StagedRollbackTest extends BaseHostJUnit4Test {
private static final String TAG = "StagedRollbackTest";
private static final int NATIVE_CRASHES_THRESHOLD = 5;
/**
* Runs the given phase of a test by calling into the device.
* Throws an exception if the test phase fails.
* <p>
* For example, <code>runPhase("testApkOnlyEnableRollback");</code>
*/
private void runPhase(String phase) throws Exception {
assertThat(runDeviceTests("com.android.tests.rollback",
"com.android.tests.rollback.StagedRollbackTest",
phase)).isTrue();
}
private static final String APK_IN_APEX_TESTAPEX_NAME = "com.android.apex.apkrollback.test";
private static final String TESTAPP_A = "com.android.cts.install.lib.testapp.A";
private static final String REASON_APP_CRASH = "REASON_APP_CRASH";
private static final String REASON_NATIVE_CRASH = "REASON_NATIVE_CRASH";
private static final String ROLLBACK_INITIATE = "ROLLBACK_INITIATE";
private static final String ROLLBACK_BOOT_TRIGGERED = "ROLLBACK_BOOT_TRIGGERED";
private static final String ROLLBACK_SUCCESS = "ROLLBACK_SUCCESS";
private WatchdogEventLogger mLogger = new WatchdogEventLogger();
@Rule
public AbandonSessionsRule mHostTestRule = new AbandonSessionsRule(this);
@Before
public void setUp() throws Exception {
deleteFiles("/system/apex/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex",
"/data/apex/active/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex");
runPhase("expireRollbacks");
mLogger.start(getDevice());
getDevice().uninstallPackage("com.android.cts.install.lib.testapp.A");
getDevice().uninstallPackage("com.android.cts.install.lib.testapp.B");
getDevice().uninstallPackage("com.android.cts.install.lib.testapp.C");
}
@After
public void tearDown() throws Exception {
getDevice().uninstallPackage("com.android.cts.install.lib.testapp.A");
getDevice().uninstallPackage("com.android.cts.install.lib.testapp.B");
getDevice().uninstallPackage("com.android.cts.install.lib.testapp.C");
mLogger.stop();
runPhase("expireRollbacks");
deleteFiles("/system/apex/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex",
"/data/apex/active/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex",
apexDataDirDeSys(APK_IN_APEX_TESTAPEX_NAME) + "*",
apexDataDirCe(APK_IN_APEX_TESTAPEX_NAME, 0) + "*",
"/system/apex/test.rebootless_apex_v*.apex",
"/data/apex/active/test.apex.rebootless*.apex");
}
/**
* Deletes files and reboots the device if necessary.
* @param files the paths of files which might contain wildcards
*/
private void deleteFiles(String... files) throws Exception {
try {
getDevice().enableAdbRoot();
boolean found = false;
for (String file : files) {
CommandResult result = getDevice().executeShellV2Command("ls " + file);
if (result.getStatus() == CommandStatus.SUCCESS) {
found = true;
break;
}
}
if (found) {
getDevice().remountSystemWritable();
for (String file : files) {
getDevice().executeShellCommand("rm -rf " + file);
}
getDevice().reboot();
}
} finally {
getDevice().disableAdbRoot();
}
}
private void waitForDeviceNotAvailable(long timeout, TimeUnit unit) {
assertWithMessage("waitForDeviceNotAvailable() timed out in %s %s", timeout, unit)
.that(getDevice().waitForDeviceNotAvailable(unit.toMillis(timeout))).isTrue();
}
/**
* Tests rolling back user data where there are multiple rollbacks for that package.
*/
@Test
public void testPreviouslyAbandonedRollbacks() throws Exception {
runPhase("testPreviouslyAbandonedRollbacks_Phase1_InstallAndAbandon");
getDevice().reboot();
runPhase("testPreviouslyAbandonedRollbacks_Phase2_Rollback");
getDevice().reboot();
runPhase("testPreviouslyAbandonedRollbacks_Phase3_VerifyRollback");
}
/**
* Tests we can enable rollback for a allowlisted app.
*/
@Test
public void testRollbackAllowlistedApp() throws Exception {
assumeTrue(hasMainlineModule());
runPhase("testRollbackAllowlistedApp_Phase1_Install");
getDevice().reboot();
runPhase("testRollbackAllowlistedApp_Phase2_VerifyInstall");
}
/**
* Tests that RollbackPackageHealthObserver is observing apk-in-apex.
*/
@Test
public void testRollbackApexWithApkCrashing() throws Exception {
pushTestApex(APK_IN_APEX_TESTAPEX_NAME + "_v1.apex");
// Install an apex with apk that crashes
runPhase("testRollbackApexWithApkCrashing_Phase1_Install");
getDevice().reboot();
// Verify apex was installed and then crash the apk
runPhase("testRollbackApexWithApkCrashing_Phase2_Crash");
// Launch the app to crash to trigger rollback
startActivity(TESTAPP_A);
// Wait for reboot to happen
waitForDeviceNotAvailable(2, TimeUnit.MINUTES);
getDevice().waitForDeviceAvailable();
// Verify rollback occurred due to crash of apk-in-apex
runPhase("testRollbackApexWithApkCrashing_Phase3_VerifyRollback");
assertThat(mLogger).eventOccurred(ROLLBACK_INITIATE, null, REASON_APP_CRASH, TESTAPP_A);
assertThat(mLogger).eventOccurred(ROLLBACK_BOOT_TRIGGERED, null, null, null);
assertThat(mLogger).eventOccurred(ROLLBACK_SUCCESS, null, null, null);
}
/**
* Tests rollback is supported correctly for rebootless apex
*/
@Test
public void testRollbackRebootlessApex() throws Exception {
pushTestApex("test.rebootless_apex_v1.apex");
runPhase("testRollbackRebootlessApex");
}
/**
* Tests only rebootless apex (if any) is rolled back when native crash happens
*/
@Test
public void testNativeWatchdogTriggersRebootlessApexRollback() throws Exception {
pushTestApex("test.rebootless_apex_v1.apex");
runPhase("testNativeWatchdogTriggersRebootlessApexRollback_Phase1_Install");
crashProcess("system_server", NATIVE_CRASHES_THRESHOLD);
getDevice().waitForDeviceAvailable();
runPhase("testNativeWatchdogTriggersRebootlessApexRollback_Phase2_Verify");
}
/**
* Tests that packages are monitored across multiple reboots.
*/
@Test
public void testWatchdogMonitorsAcrossReboots() throws Exception {
runPhase("testWatchdogMonitorsAcrossReboots_Phase1_Install");
// The first reboot will make the rollback available.
// Information about which packages are monitored will be persisted to a file before the
// second reboot, and read from disk after the second reboot.
getDevice().reboot();
getDevice().reboot();
runPhase("testWatchdogMonitorsAcrossReboots_Phase2_VerifyInstall");
// Launch the app to crash to trigger rollback
startActivity(TESTAPP_A);
// Wait for reboot to happen
waitForDeviceNotAvailable(2, TimeUnit.MINUTES);
getDevice().waitForDeviceAvailable();
runPhase("testWatchdogMonitorsAcrossReboots_Phase3_VerifyRollback");
}
private void pushTestApex(String fileName) throws Exception {
CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(getBuild());
final File apex = buildHelper.getTestFile(fileName);
try {
getDevice().enableAdbRoot();
getDevice().remountSystemWritable();
assertThat(getDevice().pushFile(apex, "/system/apex/" + fileName)).isTrue();
} finally {
getDevice().disableAdbRoot();
}
getDevice().reboot();
}
private static String apexDataDirDeSys(String apexName) {
return String.format("/data/misc/apexdata/%s", apexName);
}
private static String apexDataDirCe(String apexName, int userId) {
return String.format("/data/misc_ce/%d/apexdata/%s", userId, apexName);
}
private void startActivity(String packageName) throws Exception {
String cmd = "am start -S -a android.intent.action.MAIN "
+ "-c android.intent.category.LAUNCHER " + packageName;
getDevice().executeShellCommand(cmd);
}
/**
* True if this build has mainline modules installed.
*/
private boolean hasMainlineModule() throws Exception {
try {
runPhase("hasMainlineModule");
return true;
} catch (AssertionError ignore) {
return false;
}
}
private void crashProcess(String processName, int numberOfCrashes) throws Exception {
String pid = "";
String lastPid = "invalid";
for (int i = 0; i < numberOfCrashes; ++i) {
// This condition makes sure before we kill the process, the process is running AND
// the last crash was finished.
while ("".equals(pid) || lastPid.equals(pid)) {
pid = getDevice().executeShellCommand("pidof " + processName);
}
getDevice().executeShellCommand("kill " + pid);
lastPid = pid;
}
}
}