| /* |
| * Copyright (C) 2011 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.ddmlib.IDevice; |
| import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey; |
| import com.android.tradefed.build.IBuildInfo; |
| import com.android.tradefed.build.IDeviceBuildInfo; |
| 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.invoker.IInvocationContext; |
| import com.android.tradefed.invoker.TestInformation; |
| import com.android.tradefed.invoker.logger.InvocationMetricLogger; |
| import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey; |
| import com.android.tradefed.log.LogUtil.CLog; |
| import com.android.tradefed.observatory.IDiscoverDependencies; |
| import com.android.tradefed.result.error.DeviceErrorIdentifier; |
| import com.android.tradefed.result.error.ErrorIdentifier; |
| import com.android.tradefed.result.error.InfraErrorIdentifier; |
| import com.android.tradefed.testtype.IAbi; |
| import com.android.tradefed.testtype.IAbiReceiver; |
| import com.android.tradefed.testtype.IInvocationContextReceiver; |
| import com.android.tradefed.testtype.suite.ModuleDefinition; |
| import com.android.tradefed.util.AbiUtils; |
| import com.android.tradefed.util.FileUtil; |
| import com.android.tradefed.util.MultiMap; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * A {@link ITargetPreparer} that attempts to push any number of files from any host path to any |
| * device path. |
| * |
| * <p>Should be performed *after* a new build is flashed, and *after* DeviceSetup is run (if |
| * enabled) |
| */ |
| @OptionClass(alias = "push-file") |
| public class PushFilePreparer extends BaseTargetPreparer |
| implements IAbiReceiver, IInvocationContextReceiver, IDiscoverDependencies { |
| private static final String MEDIA_SCAN_INTENT = |
| "am broadcast -a android.intent.action.MEDIA_MOUNTED -d file://%s " |
| + "--receiver-include-background"; |
| |
| private IAbi mAbi; |
| |
| @Deprecated |
| @Option( |
| name = "push", |
| description = |
| "Deprecated. Please use push-file instead. A push-spec, formatted as " |
| + "'/localpath/to/srcfile.txt->/devicepath/to/destfile.txt' " |
| + "or '/localpath/to/srcfile.txt->/devicepath/to/destdir/'. " |
| + "May be repeated. The local path may be relative to the test cases " |
| + "build out directories " |
| + "($ANDROID_HOST_OUT_TESTCASES / $ANDROID_TARGET_OUT_TESTCASES)." |
| ) |
| private Collection<String> mPushSpecs = new ArrayList<>(); |
| |
| @Option( |
| name = "push-file", |
| description = |
| "A push-spec, specifying the local file to the path where it should be pushed" |
| + " on device. May be repeated. If multiple files are configured to be" |
| + " pushed to the same remote path, the latest one will be pushed.") |
| private MultiMap<File, String> mPushFileSpecs = new MultiMap<>(); |
| |
| @Option( |
| name = "skip-abi-filtering", |
| description = |
| "A bool to indicate we should or shouldn't skip files that match the " |
| + "architecture string name, e.g. x86, x86_64, arm64-v8. This " |
| + "is necessary when file or folder names match an architecture " |
| + "version but still need to be pushed to the device.") |
| private boolean mSkipAbiFiltering = false; |
| |
| @Option( |
| name = "backup-file", |
| description = |
| "A key/value pair, the with key specifying a device file path to be backed up, " |
| + "and the value a device file path indicating where to save the file. " |
| + "During tear-down, the values will be executed in reverse, " |
| + "restoring the backup file location to the initial location. " |
| + "May be repeated.") |
| private Map<String, String> mBackupFileSpecs = new LinkedHashMap<>(); |
| |
| @Option(name="post-push", description= |
| "A command to run on the device (with `adb shell (yourcommand)`) after all pushes " + |
| "have been attempted. Will not be run if a push fails with abort-on-push-failure " + |
| "enabled. May be repeated.") |
| private Collection<String> mPostPushCommands = new ArrayList<>(); |
| |
| @Option(name="abort-on-push-failure", description= |
| "If false, continue if pushes fail. If true, abort the Invocation on any failure.") |
| private boolean mAbortOnFailure = true; |
| |
| @Option(name="trigger-media-scan", description= |
| "After pushing files, trigger a media scan of external storage on device.") |
| private boolean mTriggerMediaScan = false; |
| |
| @Option( |
| name = "cleanup", |
| description = |
| "Whether files pushed onto device should be cleaned up after test. Note that" |
| + " the preparer does not verify that files/directories have been deleted.") |
| private boolean mCleanup = true; |
| |
| @Option( |
| name = "remount-system", |
| description = |
| "Remounts system partition to be writable " |
| + "so that files could be pushed there too") |
| private boolean mRemountSystem = false; |
| |
| @Option( |
| name = "remount-vendor", |
| description = |
| "Remounts vendor partition to be writable " |
| + "so that files could be pushed there too") |
| private boolean mRemountVendor = false; |
| |
| private Set<String> mFilesPushed = null; |
| /** If the preparer is part of a module, we can use the test module name as a search criteria */ |
| private String mModuleName = null; |
| |
| /** |
| * Helper method to only throw if mAbortOnFailure is enabled. Callers should behave as if this |
| * method may return. |
| */ |
| private void fail(String message, DeviceDescriptor descriptor, ErrorIdentifier identifier) |
| throws TargetSetupError { |
| if (shouldAbortOnFailure()) { |
| throw new TargetSetupError(message, descriptor, identifier); |
| } else { |
| // Log the error and return |
| CLog.w(message); |
| } |
| } |
| |
| /** Create the list of files to be pushed. */ |
| public final Map<String, File> getPushSpecs(ITestDevice device) throws TargetSetupError { |
| Map<String, File> remoteToLocalMapping = new LinkedHashMap<>(); |
| for (String pushspec : mPushSpecs) { |
| String[] pair = pushspec.split("->"); |
| if (pair.length != 2) { |
| fail( |
| String.format("Invalid pushspec: '%s'", Arrays.asList(pair)), |
| device.getDeviceDescriptor(), |
| InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR); |
| continue; |
| } |
| remoteToLocalMapping.put(pair[1], new File(pair[0])); |
| } |
| // Push the file structure |
| for (File local : mPushFileSpecs.keySet()) { |
| for (String remoteLocation : mPushFileSpecs.get(local)) { |
| remoteToLocalMapping.put(remoteLocation, local); |
| } |
| } |
| return remoteToLocalMapping; |
| } |
| |
| /** Whether or not to abort on push failure. */ |
| public boolean shouldAbortOnFailure() { |
| return mAbortOnFailure; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void setAbi(IAbi abi) { |
| mAbi = abi; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public IAbi getAbi() { |
| return mAbi; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void setInvocationContext(IInvocationContext invocationContext) { |
| if (invocationContext.getAttributes().get(ModuleDefinition.MODULE_NAME) != null) { |
| // Only keep the module name |
| mModuleName = |
| invocationContext.getAttributes().get(ModuleDefinition.MODULE_NAME).get(0); |
| } |
| } |
| |
| /** |
| * Resolve relative file path via {@link IBuildInfo} and test cases directories. |
| * |
| * @param buildInfo the build artifact information |
| * @param fileName relative file path to be resolved |
| * @return the file from the build info or test cases directories |
| */ |
| public File resolveRelativeFilePath(IBuildInfo buildInfo, String fileName) { |
| File src = null; |
| if (buildInfo != null) { |
| src = buildInfo.getFile(fileName); |
| if (src != null && src.exists()) { |
| return src; |
| } |
| } |
| if (buildInfo instanceof IDeviceBuildInfo) { |
| IDeviceBuildInfo deviceBuild = (IDeviceBuildInfo) buildInfo; |
| File testDir = deviceBuild.getTestsDir(); |
| List<File> scanDirs = new ArrayList<>(); |
| // If it exists, always look first in the ANDROID_TARGET_OUT_TESTCASES |
| File targetTestCases = deviceBuild.getFile(BuildInfoFileKey.TARGET_LINKED_DIR); |
| if (targetTestCases != null) { |
| scanDirs.add(targetTestCases); |
| } |
| if (testDir != null) { |
| scanDirs.add(testDir); |
| } |
| |
| if (mModuleName != null) { |
| // Use module name as a discriminant to find some files |
| if (testDir != null) { |
| try { |
| File moduleDir = |
| FileUtil.findDirectory( |
| mModuleName, scanDirs.toArray(new File[] {})); |
| if (moduleDir != null) { |
| // If the spec is pushing the module itself |
| if (mModuleName.equals(fileName)) { |
| // If that's the main binary generated by the target, we push the |
| // full directory |
| return moduleDir; |
| } |
| // Search the module directory if it exists use it in priority |
| src = FileUtil.findFile(fileName, null, moduleDir); |
| if (src != null) { |
| // Search again with filtering on ABI |
| File srcWithAbi = FileUtil.findFile(fileName, mAbi, moduleDir); |
| if (srcWithAbi != null |
| && !srcWithAbi |
| .getAbsolutePath() |
| .startsWith(src.getAbsolutePath())) { |
| // When multiple matches are found, return the one with matching |
| // ABI unless src is its parent directory. |
| return srcWithAbi; |
| } |
| return src; |
| } |
| } else { |
| CLog.d("Did not find any module directory for '%s'", mModuleName); |
| } |
| |
| } catch (IOException e) { |
| CLog.w( |
| "Something went wrong while searching for the module '%s' " |
| + "directory.", |
| mModuleName); |
| } |
| } |
| } |
| // Search top-level matches |
| for (File searchDir : scanDirs) { |
| try { |
| Set<File> allMatch = FileUtil.findFilesObject(searchDir, fileName); |
| if (allMatch.size() > 1) { |
| CLog.d( |
| "Several match for filename '%s', searching for top-level match.", |
| fileName); |
| for (File f : allMatch) { |
| // Bias toward direct child / top level nodes |
| if (f.getParent().equals(searchDir.getAbsolutePath())) { |
| return f; |
| } |
| } |
| } else if (allMatch.size() == 1) { |
| return allMatch.iterator().next(); |
| } |
| } catch (IOException e) { |
| CLog.w("Failed to find test files from directory."); |
| } |
| } |
| // Fall-back to searching everything |
| try { |
| // Search the full tests dir if no target dir is available. |
| src = FileUtil.findFile(fileName, null, scanDirs.toArray(new File[] {})); |
| if (src != null) { |
| // Search again with filtering on ABI |
| File srcWithAbi = |
| FileUtil.findFile(fileName, mAbi, scanDirs.toArray(new File[] {})); |
| if (srcWithAbi != null |
| && !srcWithAbi.getAbsolutePath().startsWith(src.getAbsolutePath())) { |
| // When multiple matches are found, return the one with matching |
| // ABI unless src is its parent directory. |
| return srcWithAbi; |
| } |
| return src; |
| } |
| } catch (IOException e) { |
| CLog.w("Failed to find test files from directory."); |
| src = null; |
| } |
| |
| if (src == null && testDir != null) { |
| // TODO(b/138416078): Once build dependency can be fixed and test required |
| // APKs are all under the test module directory, we can remove this fallback |
| // approach to do individual download from remote artifact. |
| // Try to stage the files from remote zip files. |
| src = buildInfo.stageRemoteFile(fileName, testDir); |
| if (src != null) { |
| InvocationMetricLogger.addInvocationMetrics( |
| InvocationMetricKey.STAGE_UNDEFINED_DEPENDENCY, fileName); |
| try { |
| // Search again with filtering on ABI |
| File srcWithAbi = FileUtil.findFile(fileName, mAbi, testDir); |
| if (srcWithAbi != null |
| && !srcWithAbi |
| .getAbsolutePath() |
| .startsWith(src.getAbsolutePath())) { |
| // When multiple matches are found, return the one with matching |
| // ABI unless src is its parent directory. |
| return srcWithAbi; |
| } |
| } catch (IOException e) { |
| CLog.w("Failed to find test files with matching ABI from directory."); |
| } |
| } |
| } |
| } |
| return src; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void setUp(TestInformation testInfo) |
| throws TargetSetupError, BuildError, DeviceNotAvailableException { |
| mFilesPushed = new HashSet<>(); |
| ITestDevice device = testInfo.getDevice(); |
| if (mRemountSystem) { |
| device.remountSystemWritable(); |
| } |
| if (mRemountVendor) { |
| device.remountVendorWritable(); |
| } |
| |
| // Backup files |
| for (Map.Entry<String, String> entry : mBackupFileSpecs.entrySet()) { |
| device.executeShellCommand( |
| "mv \"" + entry.getKey() + "\" \"" + entry.getValue() + "\""); |
| } |
| |
| Map<String, File> remoteToLocalMapping = getPushSpecs(device); |
| for (String remotePath : remoteToLocalMapping.keySet()) { |
| File local = remoteToLocalMapping.get(remotePath); |
| CLog.d("Trying to push local '%s' to remote '%s'", local.getPath(), remotePath); |
| evaluatePushingPair(device, testInfo.getBuildInfo(), local, remotePath); |
| } |
| |
| for (String command : mPostPushCommands) { |
| device.executeShellCommand(command); |
| } |
| |
| if (mTriggerMediaScan) { |
| String mountPoint = device.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE); |
| device.executeShellCommand(String.format(MEDIA_SCAN_INTENT, mountPoint)); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException { |
| ITestDevice device = testInfo.getDevice(); |
| if (!(e instanceof DeviceNotAvailableException) && mCleanup && mFilesPushed != null) { |
| if (mRemountSystem) { |
| device.remountSystemReadOnly(); |
| } |
| if (mRemountVendor) { |
| device.remountVendorReadOnly(); |
| } |
| for (String devicePath : mFilesPushed) { |
| device.deleteFile(devicePath); |
| } |
| // Restore files |
| for (Map.Entry<String, String> entry : mBackupFileSpecs.entrySet()) { |
| device.executeShellCommand( |
| "mv \"" + entry.getValue() + "\" \"" + entry.getKey() + "\""); |
| } |
| } |
| } |
| |
| private void evaluatePushingPair( |
| ITestDevice device, IBuildInfo buildInfo, File src, String remotePath) |
| throws TargetSetupError, DeviceNotAvailableException { |
| String localPath = src.getPath(); |
| if (!src.isAbsolute()) { |
| src = resolveRelativeFilePath(buildInfo, localPath); |
| } |
| if (src == null || !src.exists()) { |
| fail( |
| String.format("Local source file '%s' does not exist", localPath), |
| device.getDeviceDescriptor(), |
| InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND); |
| return; |
| } |
| if (src.isDirectory()) { |
| boolean deleteContentOnly = true; |
| if (!device.doesFileExist(remotePath)) { |
| device.executeShellCommand(String.format("mkdir -p \"%s\"", remotePath)); |
| deleteContentOnly = false; |
| } else if (!device.isDirectory(remotePath)) { |
| // File exists and is not a directory |
| throw new TargetSetupError( |
| String.format( |
| "Attempting to push dir '%s' to an existing device file '%s'", |
| src.getAbsolutePath(), remotePath), |
| device.getDeviceDescriptor(), |
| DeviceErrorIdentifier.FAIL_PUSH_FILE); |
| } |
| Set<String> filter = new HashSet<>(); |
| if (mAbi != null && !mSkipAbiFiltering) { |
| String currentArch = AbiUtils.getArchForAbi(mAbi.getName()); |
| filter.addAll(AbiUtils.getArchSupported()); |
| filter.remove(currentArch); |
| } |
| // TODO: Look into using syncFiles but that requires improving sync to work for unroot |
| if (!device.pushDir(src, remotePath, filter)) { |
| fail( |
| String.format( |
| "Failed to push local '%s' to remote '%s'", localPath, remotePath), |
| device.getDeviceDescriptor(), |
| DeviceErrorIdentifier.FAIL_PUSH_FILE); |
| return; |
| } else { |
| if (deleteContentOnly) { |
| remotePath += "/*"; |
| } |
| mFilesPushed.add(remotePath); |
| } |
| } else { |
| if (!device.pushFile(src, remotePath)) { |
| fail( |
| String.format( |
| "Failed to push local '%s' to remote '%s'", localPath, remotePath), |
| device.getDeviceDescriptor(), |
| DeviceErrorIdentifier.FAIL_PUSH_FILE); |
| return; |
| } else { |
| mFilesPushed.add(remotePath); |
| } |
| } |
| } |
| |
| @Override |
| public Set<String> reportDependencies() { |
| Set<String> deps = new HashSet<>(); |
| try { |
| for (File f : getPushSpecs(null).values()) { |
| // Match the resolving logic when actually pushing |
| if (!f.isAbsolute()) { |
| deps.add(f.getName()); |
| } else { |
| CLog.d( |
| "%s detected as existing. Not reported as dependency.", |
| f.getAbsolutePath()); |
| } |
| } |
| } catch (TargetSetupError e) { |
| CLog.e(e); |
| } |
| return deps; |
| } |
| |
| public boolean shouldRemountSystem() { |
| return mRemountSystem; |
| } |
| |
| public boolean shouldRemountVendor() { |
| return mRemountVendor; |
| } |
| |
| public boolean isCleanUpEnabled() { |
| return mCleanup; |
| } |
| } |