blob: 79203c1bae9a576b82b9a5d5fe10bad7ee02c664 [file] [log] [blame]
/*
* Copyright (C) 2018 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.tradefed.targetprep;
import com.android.tradefed.command.remote.DeviceDescriptor;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.ITestDevice.ApexInfo;
import com.android.tradefed.device.PackageInfo;
import com.android.tradefed.error.HarnessRuntimeException;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.error.DeviceErrorIdentifier;
import com.android.tradefed.result.error.InfraErrorIdentifier;
import com.android.tradefed.targetprep.suite.SuiteApkInstaller;
import com.android.tradefed.util.AaptParser;
import com.android.tradefed.util.BundletoolUtil;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
import com.android.tradefed.util.RunUtil;
import com.google.common.annotations.VisibleForTesting;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/*
* A {@link TargetPreparer} that attempts to install mainline modules to device
* and verify install success.
*/
@OptionClass(alias = "mainline-module-installer")
public class InstallApexModuleTargetPreparer extends SuiteApkInstaller {
private static final String APEX_DATA_DIR = "/data/apex/active/";
private static final String STAGING_DATA_DIR = "/data/app-staging/";
private static final String SESSION_DATA_DIR = "/data/apex/sessions/";
private static final String MODULE_PUSH_REMOTE_PATH = "/data/local/tmp/";
private static final String TRAIN_WITH_APEX_INSTALL_OPTION = "install-multi-package";
private static final String ENABLE_ROLLBACK_INSTALL_OPTION = "--enable-rollback";
private static final String STAGED_INSTALL_OPTION = "--staged";
private static final String ACTIVATED_APEX_SOURCEDIR_PREFIX = "data";
private static final int R_SDK_INT = 30;
// Pattern used to identify the package names from adb shell pm path.
private static final Pattern PACKAGE_REGEX = Pattern.compile("package:(.*)");
protected static final String APEX_SUFFIX = ".apex";
protected static final String APK_SUFFIX = ".apk";
protected static final String SPLIT_APKS_SUFFIX = ".apks";
protected static final String PARENT_SESSION_CREATION_CMD = "pm install-create --multi-package";
protected static final String CHILD_SESSION_CREATION_CMD = "pm install-create";
protected static final String APEX_OPTION = "--apex";
private List<ApexInfo> mTestApexInfoList = new ArrayList<>();
private List<String> mApexModulesToUninstall = new ArrayList<>();
private List<String> mApkModulesToUninstall = new ArrayList<>();
private Set<String> mMainlineModuleInfos = new HashSet<>();
private Set<String> mApkToInstall = new LinkedHashSet<>();
private List<String> mApkInstalled = new ArrayList<>();
private List<String> mSplitsInstallArgs = new ArrayList<>();
private BundletoolUtil mBundletoolUtil;
private String mDeviceSpecFilePath = "";
private boolean mOptimizeMainlineTest = false;
@Option(name = "bundletool-file-name", description = "The file name of the bundletool jar.")
private String mBundletoolFilename;
@Option(name = "train-path", description = "The absolute path of the train folder.")
protected File mTrainFolderPath;
@Option(
name = "apex-staging-wait-time",
description = "The time in ms to wait for apex staged session ready.",
isTimeVal = true)
private long mApexStagingWaitTime = 1 * 60 * 1000;
@Option(
name="extra-booting-wait-time",
description = "The extra time in ms to wait for device ready.",
isTimeVal = true)
private long mExtraBootingWaitTime = 0;
@Option(
name = "ignore-if-module-not-preloaded",
description =
"Skip installing the module(s) when the module(s) that are not "
+ "preloaded on device. Otherwise an exception will be thrown.")
private boolean mIgnoreIfNotPreloaded = false;
@Option(
name = "skip-apex-teardown",
description =
"Skip teardown if all files to be installed are apex files. "
+ "Currently, this option is only used for Test Mapping use case.")
private boolean mSkipApexTearDown = false;
@Option(
name = "enable-rollback",
description = "Add the '--enable-rollback' flag when installing modules.")
private boolean mEnableRollback = true;
@Override
public void setUp(TestInformation testInfo)
throws TargetSetupError, BuildError, DeviceNotAvailableException {
setTestInformation(testInfo);
ITestDevice device = testInfo.getDevice();
if (mTrainFolderPath != null) {
addApksToTestFiles();
}
List<File> moduleFileNames = getTestsFileName();
if (moduleFileNames.isEmpty()) {
CLog.i("No apk/apex module file to install. Skipping.");
return;
}
if (!mSkipApexTearDown) {
// Cleanup the device if skip-apex-teardown isn't set. It will always run with the
// target preparer.
cleanUpStagedAndActiveSession(device);
}
else {
mOptimizeMainlineTest = true;
}
Set<ApexInfo> activatedApexes = device.getActiveApexes();
CLog.i("Activated apex packages list before module/train installation:");
for (ApexInfo info : activatedApexes) {
CLog.i("Activated apex: %s", info.toString());
}
List<File> testAppFiles = getModulesToInstall(testInfo);
if (testAppFiles.isEmpty()) {
CLog.i("No modules are preloaded on the device, so no modules will be installed.");
return;
}
if (mOptimizeMainlineTest) {
CLog.i("Optimizing modules that are already activated in the previous test.");
testAppFiles = optimizeModuleInstallation(activatedApexes, testAppFiles, device);
if (testAppFiles.isEmpty()) {
if (!mApexModulesToUninstall.isEmpty() || !mApkModulesToUninstall.isEmpty()) {
activateApex(device);
}
// If both the list of files to be installed and uninstalled are empty, that means
// the mainline modules are the same as the previous ones.
CLog.i("All required modules are installed");
return;
}
}
if (containsApks(testAppFiles)) {
installUsingBundleTool(testInfo, testAppFiles);
if (mTestApexInfoList.isEmpty()
&& mApexModulesToUninstall.isEmpty()
&& mApkModulesToUninstall.isEmpty()) {
CLog.i("No Apex module in the train. Skipping reboot.");
return;
} else {
activateApex(device);
}
} else {
Map<File, String> appFilesAndPackages = resolveApkFiles(testInfo, testAppFiles);
installer(testInfo, appFilesAndPackages);
if (containsApex(appFilesAndPackages.keySet())
|| containsPersistentApk(appFilesAndPackages.keySet(), testInfo)
|| !mApexModulesToUninstall.isEmpty()
|| !mApkModulesToUninstall.isEmpty()) {
activateApex(device);
}
if (mTestApexInfoList.isEmpty()) {
CLog.i("Train activation succeed.");
return;
}
}
checkApexActivation(device);
}
/**
* Boot the device to activate the updated apex modules.
*
* @param device under test.
* @throws DeviceNotAvailableException if reboot fails.
*/
private void activateApex(ITestDevice device) throws DeviceNotAvailableException {
RunUtil.getDefault().sleep(mApexStagingWaitTime);
device.reboot();
// Some devices need extra waiting time after reboot to get fully ready.
if (mExtraBootingWaitTime > 0) {
RunUtil.getDefault().sleep(mExtraBootingWaitTime);
device.waitForDeviceAvailable();
// Do a second post-boot setup (by default it is just adb root)
// in case its first execution inside reboot() was not at a right time.
device.postBootSetup();
}
}
/**
* Check if all apexes are activated.
*
* @param device under test.
* @throws TargetSetupError if activation failed.
*/
protected void checkApexActivation(ITestDevice device)
throws DeviceNotAvailableException, TargetSetupError {
Set<ApexInfo> activatedApexes;
activatedApexes = device.getActiveApexes();
if (activatedApexes.isEmpty()) {
throw new TargetSetupError(
String.format(
"Failed to retrieve activated apex on device %s. Empty set returned.",
device.getSerialNumber()),
device.getDeviceDescriptor(),
DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
} else {
CLog.i("Activated apex packages list after module/train installation:");
for (ApexInfo info : activatedApexes) {
CLog.i("Activated apex: %s", info.toString());
}
}
List<ApexInfo> failToActivateApex = getModulesFailToActivate(activatedApexes);
if (!failToActivateApex.isEmpty()) {
throw new TargetSetupError(
String.format(
"Failed to activate %s on device %s.",
listApexInfo(failToActivateApex).toString(), device.getSerialNumber()),
device.getDeviceDescriptor(),
DeviceErrorIdentifier.FAIL_ACTIVATE_APEX);
}
CLog.i("Train activation succeed.");
}
/**
* Optimization for modules to reuse those who are already activated in the previous test.
*
* @param activatedApex The set of the active apexes on device
* @param testFiles List<File> of the modules that will be installed on the device.
* @param device the {@link ITestDevice}
* @return A List<File> of the modules that will be installed on the device.
*/
private List<File> optimizeModuleInstallation(Set<ApexInfo> activatedApex, List<File> testFiles,
ITestDevice device) throws DeviceNotAvailableException, TargetSetupError {
// Get apexes that got activated in the previous test invocation.
Set<String> apexInData = getApexInData(activatedApex);
// Get the apk files that are already installed on the device.
Set<String> apkModuleInData = getApkModuleInData(activatedApex, device);
// Get the apex files that are not used by the current test and will be uninstalled.
mApexModulesToUninstall.addAll(getModulesToUninstall(apexInData, testFiles, device));
// Get the apk files that are not used by the current test and will be uninstalled.
mApkModulesToUninstall.addAll(getModulesToUninstall(apkModuleInData, testFiles, device));
for (String m : mApexModulesToUninstall) {
CLog.i("Uninstalling apex module: %s", m);
uninstallPackage(device, m);
}
for (String packageName : mApkModulesToUninstall) {
CLog.i("Uninstalling apk module: %s", packageName);
uninstallPackage(device, packageName);
}
return testFiles;
}
/**
* Get a set of modules that will be uninstalled.
*
* @param modulesInData A Set<String> of modules that are installed on the /data directory.
* @param testFiles A List<File> of modules that will be installed on the device.
* @param device the {@link ITestDevice}
* @return A Set<String> of modules that will be uninstalled on the device.
*/
Set<String> getModulesToUninstall(Set<String> modulesInData,
List<File> testFiles, ITestDevice device) throws TargetSetupError {
Set<String> unInstallModules = new HashSet<>(modulesInData);
List<File> filesToSkipInstall = new ArrayList<>();
for (File testFile : testFiles) {
String packageName = parsePackageName(testFile, device.getDeviceDescriptor());
for (String moduleInData : modulesInData) {
if (moduleInData.equals(packageName)) {
unInstallModules.remove(moduleInData);
filesToSkipInstall.add(testFile);
}
}
}
// Update the modules to be installed based on what will not be installed.
testFiles.removeAll(filesToSkipInstall);
return unInstallModules;
}
/**
* Return a set of apex files that are already installed on the /data directory.
*/
Set<String> getApexInData(Set<ApexInfo> activatedApexes) {
Set<String> apexInData = new HashSet<>();
for (ApexInfo apex : activatedApexes) {
if (apex.sourceDir.startsWith(APEX_DATA_DIR, 0) ||
apex.sourceDir.startsWith(STAGING_DATA_DIR, 0) ||
apex.sourceDir.startsWith(SESSION_DATA_DIR, 0)) {
apexInData.add(apex.name);
}
}
return apexInData;
}
/**
* Return a set of apk modules by excluding the apex modules from the given mainline modules.
*/
Set<String> getApkModules(Set<String> moduleInfos, Set<ApexInfo> activatedApexes) {
Set<String> apexModules = new HashSet<>();
for (ApexInfo apex : activatedApexes) {
apexModules.add(apex.name);
}
moduleInfos.removeAll(apexModules);
return moduleInfos;
}
/**
* Return a set of apk modules that are already installed on the /data directory.
*/
Set<String> getApkModuleInData(Set<ApexInfo> activatedApexes, ITestDevice device)
throws DeviceNotAvailableException {
Set<String> apkModuleInData = new HashSet<>();
try {
// Get all mainline modules based on the MODULE METADATA on the device.
mMainlineModuleInfos = device.getMainlineModuleInfo();
} catch (UnsupportedOperationException usoe) {
CLog.e("Failed to query modules based on the MODULE_METADATA on the device - "
+ "unsupported operation, returning an empty list of apk modules.");
return apkModuleInData;
}
// Get the apk modules based on mainline module info and the activated apex modules.
Set<String> apkModules = getApkModules(mMainlineModuleInfos, activatedApexes);
for (String apkModule : apkModules) {
String output = device.executeShellCommand(String.format("pm path %s", apkModule));
if (output != null) {
Matcher m = PACKAGE_REGEX.matcher(output);
while (m.find()) {
String packageName = m.group(1);
CLog.i("Activates apk module: %s, path: %s", apkModule, packageName);
if (packageName.startsWith("/data/app/")) {
apkModuleInData.add(apkModule);
}
}
}
}
return apkModuleInData;
}
@Override
public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
if (mOptimizeMainlineTest) {
if (!mApkInstalled.isEmpty() && mMainlineModuleInfos.isEmpty()) {
CLog.d("Proceeding tearDown as no MODULE METADATA existing on the device.");
}
else {
CLog.d("Skipping tearDown as the installed modules may be used for the next test.");
return;
}
}
ITestDevice device = testInfo.getDevice();
if (e instanceof DeviceNotAvailableException) {
CLog.e("Device %s is not available. Teardown() skipped.", device.getSerialNumber());
return;
} else {
if (mTestApexInfoList.isEmpty() && getApkInstalled().isEmpty()) {
super.tearDown(testInfo, e);
} else {
if (mTestApexInfoList.isEmpty()) {
for (String apkPkgName : getApkInstalled()) {
uninstallPackage(device, apkPkgName);
}
} else {
for (ApexInfo apex : mTestApexInfoList) {
String output =
device.executeShellCommand(
String.format("pm rollback-app %s", apex.name));
// Rolling back one newly installed module will rollback all other newly
// installed modules.
if (output.contains("Success")) {
break;
} else {
throw new HarnessRuntimeException(
String.format(
"Failed to rollback %s, Output: %s", apex.name, output),
DeviceErrorIdentifier.APEX_ROLLBACK_FAILED);
}
}
CLog.i("Wait for rollback fully done.");
RunUtil.getDefault().sleep(mApexStagingWaitTime);
CLog.i("Device Rebooting");
device.reboot();
CLog.i("Reboot finished. Wait for rollback fully propagate.");
RunUtil.getDefault().sleep(mApexStagingWaitTime);
device.waitForDeviceAvailable();
}
}
}
}
/**
* Initializes the bundletool util for this class.
*
* @param testInfo the {@link TestInformation} for the invocation.
* @throws TargetSetupError if bundletool cannot be found.
*/
protected void initBundletoolUtil(TestInformation testInfo) throws TargetSetupError {
if (mBundletoolUtil != null) {
return;
}
File bundletoolJar;
File f = new File(getBundletoolFileName());
if (!f.isAbsolute()) {
bundletoolJar = getLocalPathForFilename(testInfo, getBundletoolFileName());
} else {
bundletoolJar = f;
}
if (bundletoolJar == null) {
throw new TargetSetupError(
String.format("Failed to find bundletool jar %s.", getBundletoolFileName()),
testInfo.getDevice().getDeviceDescriptor(),
InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND);
}
mBundletoolUtil = new BundletoolUtil(bundletoolJar);
}
/**
* Initializes the path to the device spec file.
*
* @param device the {@link ITestDevice} to install the train.
* @throws TargetSetupError if fails to generate the device spec file.
*/
private void initDeviceSpecFilePath(ITestDevice device) throws TargetSetupError {
if (!mDeviceSpecFilePath.equals("")) {
return;
}
try {
mDeviceSpecFilePath = getBundletoolUtil().generateDeviceSpecFile(device);
} catch (IOException e) {
throw new TargetSetupError(
String.format(
"Failed to generate device spec file on %s.", device.getSerialNumber()),
e,
device.getDeviceDescriptor());
}
}
/**
* Extracts and returns splits for the specified apks.
*
* @param testInfo the {@link TestInformation}
* @param moduleFile The module file to extract the splits from.
* @return a File[] containing the splits.
* @throws TargetSetupError if bundletool cannot be found or device spec file fails to generate.
*/
protected List<File> getSplitsForApks(TestInformation testInfo, File moduleFile)
throws TargetSetupError {
initBundletoolUtil(testInfo);
initDeviceSpecFilePath(testInfo.getDevice());
File splitsDir =
getBundletoolUtil()
.extractSplitsFromApks(
moduleFile,
mDeviceSpecFilePath,
testInfo.getDevice(),
testInfo.getBuildInfo());
if (splitsDir == null || splitsDir.listFiles() == null) {
return null;
}
return Arrays.asList(splitsDir.listFiles());
}
/**
* Gets the modules that should be installed on the train, based on the modules preloaded on the
* device. Modules that are not preloaded will not be installed.
*
* @param testInfo the {@link TestInformation}
* @return List<String> of the modules that should be installed on the device.
* @throws DeviceNotAvailableException when device is not available.
* @throws TargetSetupError when mandatory modules are not installed, or module cannot be
* installed.
*/
public List<File> getModulesToInstall(TestInformation testInfo)
throws DeviceNotAvailableException, TargetSetupError {
// Get all preloaded modules for the device.
ITestDevice device = testInfo.getDevice();
Set<String> installedPackages = new HashSet<>(device.getInstalledPackageNames());
Set<ApexInfo> installedApexes = new HashSet<>(device.getActiveApexes());
for (ApexInfo installedApex : installedApexes) {
installedPackages.add(installedApex.name);
}
Set<String> trainInstalledPackages = new HashSet<>();
List<File> moduleFileNames = getTestsFileName();
List<File> moduleNamesToInstall = new ArrayList<>();
for (File moduleFileName : moduleFileNames) {
// getLocalPathForFilename throws if apk not found
File moduleFile = moduleFileName;
if (!moduleFile.isAbsolute()) {
moduleFile = getLocalPathForFilename(testInfo, moduleFileName.getName());
}
String modulePackageName = "";
if (moduleFile.getName().endsWith(SPLIT_APKS_SUFFIX)) {
List<File> splits = getSplitsForApks(testInfo, moduleFile);
if (splits == null) {
// Bundletool failed to extract splits.
CLog.w(
"Apks %s is not available on device %s and will not be installed.",
moduleFileName, mDeviceSpecFilePath);
continue;
}
modulePackageName = parsePackageName(splits.get(0), device.getDeviceDescriptor());
} else {
modulePackageName = parsePackageName(moduleFile, device.getDeviceDescriptor());
}
if (installedPackages.contains(modulePackageName)) {
CLog.i("Found preloaded module for %s.", modulePackageName);
moduleNamesToInstall.add(moduleFile);
if (trainInstalledPackages.contains(modulePackageName)) {
throw new TargetSetupError(
String.format(
"Mainline module %s is listed for install more than once.",
modulePackageName),
InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR);
}
trainInstalledPackages.add(modulePackageName);
} else {
if (!mIgnoreIfNotPreloaded) {
CLog.i(
"The following modules are preloaded on the device %s",
installedPackages);
throw new TargetSetupError(
String.format(
"Mainline module %s is not preloaded on the device "
+ "but is in the input lists.",
modulePackageName),
device.getDeviceDescriptor(),
DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
}
CLog.i(
"The module package %s is not preloaded on the device but is included in "
+ "the train.",
modulePackageName);
}
}
// Log the modules that are not included in the train.
Set<String> nonTrainPackages = new HashSet<>(installedPackages);
nonTrainPackages.removeAll(trainInstalledPackages);
if (!nonTrainPackages.isEmpty()) {
CLog.i(
"The following modules are preloaded on the device, but not included in the "
+ "train: %s",
nonTrainPackages);
}
return moduleNamesToInstall;
}
// TODO(b/124461631): Remove after ddmlib supports install-multi-package.
@Override
protected void installer(TestInformation testInfo, Map<File, String> testAppFileNames)
throws TargetSetupError, DeviceNotAvailableException {
if (containsApex(testAppFileNames.keySet())) {
mTestApexInfoList = collectApexInfoFromApexModules(testAppFileNames, testInfo);
}
installTrain(testInfo, new ArrayList<>(testAppFileNames.keySet()));
}
/**
* Attempts to install a mainline train containing apex on the device.
*
* @param testInfo the {@link TestInformation}
* @param moduleFilenames List of String. The list of filenames of the mainline modules to be
* installed.
*/
protected void installTrain(
TestInformation testInfo, List<File> moduleFilenames)
throws TargetSetupError, DeviceNotAvailableException {
ITestDevice device = testInfo.getDevice();
List<String> apkPackageNames = new ArrayList<>();
for (File moduleFile : moduleFilenames) {
if (!device.pushFile(moduleFile, MODULE_PUSH_REMOTE_PATH + moduleFile.getName())) {
throw new TargetSetupError(
String.format(
"Failed to push local '%s' to remote '%s'",
moduleFile.getAbsolutePath(), MODULE_PUSH_REMOTE_PATH + moduleFile.getName()),
device.getDeviceDescriptor(),
DeviceErrorIdentifier.FAIL_PUSH_FILE);
} else {
CLog.d("%s pushed successfully to %s.", moduleFile.getName(), MODULE_PUSH_REMOTE_PATH + moduleFile.getName());
}
if (moduleFile.getName().endsWith(APK_SUFFIX)) {
String packageName = parsePackageName(moduleFile, device.getDeviceDescriptor());
apkPackageNames.add(packageName);
}
}
String cmd = PARENT_SESSION_CREATION_CMD + " " + STAGED_INSTALL_OPTION;
if (mEnableRollback) {
cmd += " " + ENABLE_ROLLBACK_INSTALL_OPTION;
}
CommandResult res = device.executeShellV2Command(cmd + " | egrep -o -e '[0-9]+'");
String parentSessionId;
if (res.getStatus() == CommandStatus.SUCCESS) {
parentSessionId = res.getStdout();
CLog.d("Parent session %s created successfully. ", parentSessionId);
} else {
throw new TargetSetupError(
String.format("Failed to create parent session. Error: %s", res.getStderr()),
device.getDeviceDescriptor());
}
for (File moduleFile : moduleFilenames) {
String childSessionId = null;
if (moduleFile.getName().endsWith(APEX_SUFFIX)) {
if (mEnableRollback) {
res = device.executeShellV2Command(String.format("%s %s %s %s | egrep -o -e '[0-9]+'", CHILD_SESSION_CREATION_CMD, APEX_OPTION, STAGED_INSTALL_OPTION, ENABLE_ROLLBACK_INSTALL_OPTION));
} else {
res = device.executeShellV2Command(String.format("%s %s %s | egrep -o -e '[0-9]+'", CHILD_SESSION_CREATION_CMD, APEX_OPTION, STAGED_INSTALL_OPTION));
}
} else {
if (mEnableRollback) {
res = device.executeShellV2Command(String.format("%s %s %s | egrep -o -e '[0-9]+'", CHILD_SESSION_CREATION_CMD, STAGED_INSTALL_OPTION, ENABLE_ROLLBACK_INSTALL_OPTION));
} else {
res = device.executeShellV2Command(String.format("%s %s | egrep -o -e '[0-9]+'", CHILD_SESSION_CREATION_CMD, STAGED_INSTALL_OPTION));
}
}
if (res.getStatus() == CommandStatus.SUCCESS) {
childSessionId = res.getStdout();
CLog.d("Child session %s created successfully for %s. ", childSessionId, moduleFile.getName());
} else {
throw new TargetSetupError(
String.format(
"Failed to create child session for %s. Error: %s", moduleFile.getName(), res.getStderr()),
device.getDeviceDescriptor());
}
res = device.executeShellV2Command(
String.format(
"pm install-write -S %d %s %s %s",
moduleFile.length(),
childSessionId,
parsePackageName(moduleFile, device.getDeviceDescriptor()),
MODULE_PUSH_REMOTE_PATH + moduleFile.getName()));
if (res.getStatus() == CommandStatus.SUCCESS) {
CLog.d("Successfully wrote %s to session %s. ", moduleFile.getName(), childSessionId);
} else {
throw new TargetSetupError(
String.format("Failed to write %s to session %s. Error: %s", moduleFile.getName(), childSessionId, res.getStderr()),
device.getDeviceDescriptor());
}
res = device.executeShellV2Command(
String.format(
"pm install-add-session " + parentSessionId + " " + childSessionId));
if (res.getStatus() != CommandStatus.SUCCESS) {
throw new TargetSetupError(
String.format("Failed to add child session %s to parent session %s. Error: %s", childSessionId, parentSessionId, res.getStderr()),
device.getDeviceDescriptor());
}
}
res = device.executeShellV2Command("pm install-commit " + parentSessionId);
// Wait until all apexes are fully staged and ready.
// TODO: should have adb level solution b/130039562
RunUtil.getDefault().sleep(mApexStagingWaitTime);
if (res.getStatus() == CommandStatus.SUCCESS) {
CLog.d("Train is staged successfully. Stdout: %s.", res.getStdout());
} else {
throw new TargetSetupError(
String.format(
"Failed to commit %s on %s. Error: %s",
parentSessionId, device.getSerialNumber(), res.getStderr()),
device.getDeviceDescriptor(),
DeviceErrorIdentifier.APK_INSTALLATION_FAILED);
}
mApkInstalled.addAll(apkPackageNames);
}
/**
* Attempts to install mainline module(s) using bundletool.
*
* @param testInfo the {@link TestInformation}
* @param testAppFileNames the filenames of the preloaded modules to install.
*/
protected void installUsingBundleTool(TestInformation testInfo, List<File> testAppFileNames)
throws TargetSetupError, DeviceNotAvailableException {
initBundletoolUtil(testInfo);
initDeviceSpecFilePath(testInfo.getDevice());
if (testAppFileNames.size() == 1) {
// Installs single .apks module.
installSingleModuleUsingBundletool(
testInfo, mDeviceSpecFilePath, testAppFileNames.get(0));
} else {
installMultipleModuleUsingBundletool(testInfo, mDeviceSpecFilePath, testAppFileNames);
}
mApkInstalled.addAll(mApkToInstall);
}
/**
* Attempts to install a single mainline module(.apks) using bundletool.
*
* @param testInfo the {@link TestInformation}
* @param deviceSpecFilePath the spec file of the test device
* @param apkFile the file of the .apks
*/
private void installSingleModuleUsingBundletool(
TestInformation testInfo, String deviceSpecFilePath, File apkFile)
throws TargetSetupError, DeviceNotAvailableException {
// No need to resolve we have the single .apks file needed.
File apks = apkFile;
// Rename the extracted files and add the file to filename list.
List<File> splits = getSplitsForApks(testInfo, apks);
ITestDevice device = testInfo.getDevice();
if (splits == null || splits.isEmpty()) {
throw new TargetSetupError(
String.format("Extraction for %s failed. No apk/apex is extracted.", apkFile),
device.getDeviceDescriptor());
}
// Install .apks that contain apex module.
if (containsApex(splits)) {
Map<File, String> appFilesAndPackages = new LinkedHashMap<>();
appFilesAndPackages.put(
splits.get(0), parsePackageName(splits.get(0), device.getDeviceDescriptor()));
super.installer(testInfo, appFilesAndPackages);
mTestApexInfoList = collectApexInfoFromApexModules(appFilesAndPackages, testInfo);
} else {
// Install .apks that contain apk module.
getBundletoolUtil().installApks(apks, device);
mApkToInstall.add(parsePackageName(splits.get(0), device.getDeviceDescriptor()));
}
return;
}
/**
* Attempts to install multiple mainline modules using bundletool. Modules can be any
* combination of .apk, .apex or .apks.
*
* @param testInfo the {@link TestInformation}
* @param deviceSpecFilePath the spec file of the test device
* @param testAppFileNames the list of preloaded modules to install.
*/
private void installMultipleModuleUsingBundletool(
TestInformation testInfo, String deviceSpecFilePath, List<File> testAppFileNames)
throws TargetSetupError, DeviceNotAvailableException {
ITestDevice device = testInfo.getDevice();
for (File moduleFileName : testAppFileNames) {
File moduleFile;
if (!moduleFileName.isAbsolute()) {
moduleFile = getLocalPathForFilename(testInfo, moduleFileName.getName());
} else {
moduleFile = moduleFileName;
}
if (moduleFileName.getName().endsWith(SPLIT_APKS_SUFFIX)) {
List<File> splits = getSplitsForApks(testInfo, moduleFile);
String splitsArgs = createInstallArgsForSplit(splits, device);
mSplitsInstallArgs.add(splitsArgs);
} else {
if (moduleFileName.getName().endsWith(APEX_SUFFIX)) {
ApexInfo apexInfo = retrieveApexInfo(moduleFile, device.getDeviceDescriptor());
mTestApexInfoList.add(apexInfo);
} else {
mApkToInstall.add(parsePackageName(moduleFile, device.getDeviceDescriptor()));
}
mSplitsInstallArgs.add(moduleFile.getAbsolutePath());
}
}
List<String> installCmd = new ArrayList<>();
installCmd.add(TRAIN_WITH_APEX_INSTALL_OPTION);
if (mEnableRollback) {
installCmd.add(ENABLE_ROLLBACK_INSTALL_OPTION);
}
for (String arg : mSplitsInstallArgs) {
installCmd.add(arg);
}
device.waitForDeviceAvailable();
String log = device.executeAdbCommand(installCmd.toArray(new String[0]));
if (log.contains("Success")) {
CLog.d("Train is staged successfully. Output: %s.", log);
} else {
throw new TargetSetupError(
String.format(
"Failed to stage train on device %s. Cmd is: %s. Error log: %s.",
device.getSerialNumber(), installCmd.toString(), log),
device.getDeviceDescriptor(),
DeviceErrorIdentifier.FAIL_ACTIVATE_APEX);
}
}
/**
* Retrieves ApexInfo which contains packageName and versionCode from the given apex file.
*
* @param testApexFile The apex file we retrieve information from.
* @return an {@link ApexInfo} containing the packageName and versionCode of the given file
* @throws TargetSetupError if aapt parser failed to parse the file.
*/
@VisibleForTesting
protected ApexInfo retrieveApexInfo(File testApexFile, DeviceDescriptor deviceDescriptor)
throws TargetSetupError {
AaptParser parser = AaptParser.parse(testApexFile);
if (parser == null) {
throw new TargetSetupError(
"apex installed but AaptParser failed",
deviceDescriptor,
DeviceErrorIdentifier.AAPT_PARSER_FAILED);
}
return new ApexInfo(parser.getPackageName(), Long.parseLong(parser.getVersionCode()));
}
/**
* Gets the keyword (e.g., 'tzdata' for com.android.tzdata.apex) from the apex package name.
*
* @param packageName The package name of the apex file.
* @return a string The keyword of the apex package name.
*/
protected String getModuleKeywordFromApexPackageName(String packageName) {
String[] components = packageName.split("\\.");
return components[components.length - 1];
}
/* Helper method to format List<ApexInfo> to List<String>. */
private ArrayList<String> listApexInfo(List<ApexInfo> list) {
ArrayList<String> res = new ArrayList<String>();
for (ApexInfo testApexInfo : list) {
res.add(testApexInfo.toString());
}
return res;
}
/* Checks if the app file is apex or not */
private boolean isApex(File file) {
if (file.getName().endsWith(APEX_SUFFIX)) {
return true;
}
return false;
}
/** Checks if the apps need to be installed contains apex. */
private boolean containsApex(Collection<File> testFileNames) {
for (File filename : testFileNames) {
if (filename.getName().endsWith(APEX_SUFFIX)) {
return true;
}
}
return false;
}
/**
* Checks if the apps need to be installed contains apex.
*
* @param testFileNames The list of the test modules
*/
private boolean containsApks(List<File> testFileNames) {
for (File filename : testFileNames) {
if (filename.getName().endsWith(SPLIT_APKS_SUFFIX)) {
return true;
}
}
return false;
}
/**
* Cleans up data/apex/active. data/apex/sessions, data/app-staging.
*
* @param device The test device
*/
private void cleanUpStagedAndActiveSession(ITestDevice device)
throws DeviceNotAvailableException {
boolean reboot = false;
if (!mTestApexInfoList.isEmpty()) {
device.deleteFile(APEX_DATA_DIR + "*");
device.deleteFile(STAGING_DATA_DIR + "*");
device.deleteFile(SESSION_DATA_DIR + "*");
reboot = true;
} else {
if (!device.executeShellV2Command("ls " + APEX_DATA_DIR).getStdout().isEmpty()) {
device.deleteFile(APEX_DATA_DIR + "*");
reboot = true;
}
if (!device.executeShellV2Command("ls " + STAGING_DATA_DIR).getStdout().isEmpty()) {
device.deleteFile(STAGING_DATA_DIR + "*");
reboot = true;
}
if (!device.executeShellV2Command("ls " + SESSION_DATA_DIR).getStdout().isEmpty()) {
device.deleteFile(SESSION_DATA_DIR + "*");
reboot = true;
}
}
if (reboot) {
device.reboot();
}
}
/**
* Creates the install args for the split .apks.
*
* @param splits The directory that split apk/apex get extracted to
* @param device The test device
* @return a {@link String} representing the install args for the split apks.
*/
private String createInstallArgsForSplit(List<File> splits, ITestDevice device)
throws TargetSetupError {
String splitsArgs = "";
for (File f : splits) {
if (f.getName().endsWith(APEX_SUFFIX)) {
ApexInfo apexInfo = retrieveApexInfo(f, device.getDeviceDescriptor());
mTestApexInfoList.add(apexInfo);
}
if (f.getName().endsWith(APK_SUFFIX)) {
mApkToInstall.add(parsePackageName(f, device.getDeviceDescriptor()));
}
if (!splitsArgs.isEmpty()) {
splitsArgs += ":" + f.getAbsolutePath();
} else {
splitsArgs += f.getAbsolutePath();
}
}
return splitsArgs;
}
/**
* Checks if the input files contain any persistent apk.
*
* @param testAppFileNames The list of the file names of the modules to install
* @param testInfo The {@link TestInformation}
* @return <code>true</code> if the input files contains a persistent apk module.
*/
protected boolean containsPersistentApk(
Collection<File> testAppFileNames, TestInformation testInfo)
throws TargetSetupError, DeviceNotAvailableException {
for (File moduleFileName : testAppFileNames) {
if (isPersistentApk(moduleFileName, testInfo)) {
return true;
}
}
return false;
}
/**
* Checks if an apk is a persistent apk.
*
* @param filename The apk module file to check
* @param testInfo The {@link TestInformation}
* @return <code>true</code> if this is a persistent apk module.
*/
protected boolean isPersistentApk(File filename, TestInformation testInfo)
throws TargetSetupError, DeviceNotAvailableException {
if (!filename.getName().endsWith(APK_SUFFIX)) {
return false;
}
PackageInfo pkgInfo =
testInfo.getDevice()
.getAppPackageInfo(
parsePackageName(
filename, testInfo.getDevice().getDeviceDescriptor()));
return pkgInfo.isPersistentApp();
}
/**
* Collects apex info from the apex modules for activation check.
*
* @param testAppFileNames The list of the file names of the modules to install
* @param testInfo The {@link TestInformation}
* @return a list containing the apexinfo of the apex modules in the input file lists
*/
protected List<ApexInfo> collectApexInfoFromApexModules(
Map<File, String> testAppFileNames, TestInformation testInfo) throws TargetSetupError {
List<ApexInfo> apexInfoList = new ArrayList<>();
for (File appFile : testAppFileNames.keySet()) {
if (isApex(appFile)) {
ApexInfo apexInfo =
retrieveApexInfo(appFile, testInfo.getDevice().getDeviceDescriptor());
apexInfoList.add(apexInfo);
}
}
return apexInfoList;
}
/**
* Get modules that failed to be activated.
*
* @param activatedApexes The set of the active apexes on device
* @return a list containing the apexinfo of the input apex modules that failed to be activated.
*/
protected List<ApexInfo> getModulesFailToActivate(Set<ApexInfo> activatedApexes)
throws DeviceNotAvailableException, TargetSetupError {
List<ApexInfo> failToActivateApex = new ArrayList<ApexInfo>();
HashMap<String, ApexInfo> activatedApexInfo = new HashMap<>();
for (ApexInfo info : activatedApexes) {
activatedApexInfo.put(info.name, info);
}
for (ApexInfo testApexInfo : mTestApexInfoList) {
if (!activatedApexInfo.containsKey(testApexInfo.name)) {
failToActivateApex.add(testApexInfo);
} else if (activatedApexInfo.get(testApexInfo.name).versionCode
!= testApexInfo.versionCode) {
failToActivateApex.add(testApexInfo);
} else {
String sourceDir = activatedApexInfo.get(testApexInfo.name).sourceDir;
// Activated apex sourceDir starts with "/data"
if (getDevice().checkApiLevelAgainstNextRelease(R_SDK_INT)
&& !sourceDir.startsWith(ACTIVATED_APEX_SOURCEDIR_PREFIX, 1)) {
failToActivateApex.add(testApexInfo);
}
}
}
return failToActivateApex;
}
protected void addApksToTestFiles() {
File[] filesUnderTrainFolder = mTrainFolderPath.listFiles();
Arrays.sort(filesUnderTrainFolder, (a, b) -> a.getName().compareTo(b.getName()));
for (File f : filesUnderTrainFolder) {
if (f.getName().endsWith(".apks")) {
getTestsFileName().add(f);
}
}
}
@VisibleForTesting
protected String getBundletoolFileName() {
return mBundletoolFilename;
}
@VisibleForTesting
protected BundletoolUtil getBundletoolUtil() {
return mBundletoolUtil;
}
@VisibleForTesting
protected List<String> getApkInstalled() {
return mApkInstalled;
}
@VisibleForTesting
public void setSkipApexTearDown(boolean skip) {
mSkipApexTearDown = skip;
}
@VisibleForTesting
public void setIgnoreIfNotPreloaded(boolean skip) {
mIgnoreIfNotPreloaded = skip;
}
}