1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tradefed.targetprep; 18 19 import com.android.ddmlib.IDevice; 20 import com.android.ddmlib.Log; 21 import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey; 22 import com.android.tradefed.build.IBuildInfo; 23 import com.android.tradefed.build.IDeviceBuildInfo; 24 import com.android.tradefed.command.remote.DeviceDescriptor; 25 import com.android.tradefed.config.Option; 26 import com.android.tradefed.config.OptionClass; 27 import com.android.tradefed.device.DeviceNotAvailableException; 28 import com.android.tradefed.device.ITestDevice; 29 import com.android.tradefed.invoker.IInvocationContext; 30 import com.android.tradefed.invoker.TestInformation; 31 import com.android.tradefed.log.LogUtil.CLog; 32 import com.android.tradefed.testtype.IAbi; 33 import com.android.tradefed.testtype.IAbiReceiver; 34 import com.android.tradefed.testtype.IInvocationContextReceiver; 35 import com.android.tradefed.testtype.suite.ModuleDefinition; 36 import com.android.tradefed.util.AbiUtils; 37 import com.android.tradefed.util.FileUtil; 38 import com.android.tradefed.util.MultiMap; 39 40 import java.io.File; 41 import java.io.IOException; 42 import java.util.ArrayList; 43 import java.util.Arrays; 44 import java.util.Collection; 45 import java.util.HashSet; 46 import java.util.LinkedHashMap; 47 import java.util.List; 48 import java.util.Map; 49 import java.util.Set; 50 51 /** 52 * A {@link ITargetPreparer} that attempts to push any number of files from any host path to any 53 * device path. 54 * 55 * <p>Should be performed *after* a new build is flashed, and *after* DeviceSetup is run (if 56 * enabled) 57 */ 58 @OptionClass(alias = "push-file") 59 public class PushFilePreparer extends BaseTargetPreparer 60 implements IAbiReceiver, IInvocationContextReceiver { 61 private static final String LOG_TAG = "PushFilePreparer"; 62 private static final String MEDIA_SCAN_INTENT = 63 "am broadcast -a android.intent.action.MEDIA_MOUNTED -d file://%s " 64 + "--receiver-include-background"; 65 66 private IAbi mAbi; 67 68 @Deprecated 69 @Option( 70 name = "push", 71 description = 72 "Deprecated. Please use push-file instead. A push-spec, formatted as " 73 + "'/localpath/to/srcfile.txt->/devicepath/to/destfile.txt' " 74 + "or '/localpath/to/srcfile.txt->/devicepath/to/destdir/'. " 75 + "May be repeated. The local path may be relative to the test cases " 76 + "build out directories " 77 + "($ANDROID_HOST_OUT_TESTCASES / $ANDROID_TARGET_OUT_TESTCASES)." 78 ) 79 private Collection<String> mPushSpecs = new ArrayList<>(); 80 81 @Option( 82 name = "push-file", 83 description = 84 "A push-spec, specifying the local file to the path where it should be pushed on " 85 + "device. May be repeated. If multiple files are configured to be pushed " 86 + "to the same remote path, the latest one will be pushed.") 87 private MultiMap<File, String> mPushFileSpecs = new MultiMap<>(); 88 89 @Option( 90 name = "backup-file", 91 description = 92 "A key/value pair, the with key specifying a device file path to be backed up, " 93 + "and the value a device file path indicating where to save the file. " 94 + "During tear-down, the values will be executed in reverse, " 95 + "restoring the backup file location to the initial location. " 96 + "May be repeated.") 97 private Map<String, String> mBackupFileSpecs = new LinkedHashMap<>(); 98 99 @Option(name="post-push", description= 100 "A command to run on the device (with `adb shell (yourcommand)`) after all pushes " + 101 "have been attempted. Will not be run if a push fails with abort-on-push-failure " + 102 "enabled. May be repeated.") 103 private Collection<String> mPostPushCommands = new ArrayList<>(); 104 105 @Option(name="abort-on-push-failure", description= 106 "If false, continue if pushes fail. If true, abort the Invocation on any failure.") 107 private boolean mAbortOnFailure = true; 108 109 @Option(name="trigger-media-scan", description= 110 "After pushing files, trigger a media scan of external storage on device.") 111 private boolean mTriggerMediaScan = false; 112 113 @Option(name="cleanup", description = "Whether files pushed onto device should be cleaned up " 114 + "after test. Note that the preparer does not verify that files/directories have " 115 + "been deleted.") 116 private boolean mCleanup = false; 117 118 @Option( 119 name = "remount-system", 120 description = 121 "Remounts system partition to be writable " 122 + "so that files could be pushed there too") 123 private boolean mRemountSystem = false; 124 125 @Option( 126 name = "remount-vendor", 127 description = 128 "Remounts vendor partition to be writable " 129 + "so that files could be pushed there too") 130 private boolean mRemountVendor = false; 131 132 private Set<String> mFilesPushed = null; 133 /** If the preparer is part of a module, we can use the test module name as a search criteria */ 134 private String mModuleName = null; 135 136 /** 137 * Helper method to only throw if mAbortOnFailure is enabled. Callers should behave as if this 138 * method may return. 139 */ fail(String message, DeviceDescriptor descriptor)140 private void fail(String message, DeviceDescriptor descriptor) throws TargetSetupError { 141 if (shouldAbortOnFailure()) { 142 throw new TargetSetupError(message, descriptor); 143 } else { 144 // Log the error and return 145 Log.w(LOG_TAG, message); 146 } 147 } 148 149 /** Create the list of files to be pushed. */ getPushSpecs(DeviceDescriptor descriptor)150 public final Map<String, File> getPushSpecs(DeviceDescriptor descriptor) 151 throws TargetSetupError { 152 Map<String, File> remoteToLocalMapping = new LinkedHashMap<>(); 153 for (String pushspec : mPushSpecs) { 154 String[] pair = pushspec.split("->"); 155 if (pair.length != 2) { 156 fail(String.format("Invalid pushspec: '%s'", Arrays.asList(pair)), descriptor); 157 continue; 158 } 159 remoteToLocalMapping.put(pair[1], new File(pair[0])); 160 } 161 // Push the file structure 162 for (File local : mPushFileSpecs.keySet()) { 163 for (String remoteLocation : mPushFileSpecs.get(local)) { 164 remoteToLocalMapping.put(remoteLocation, local); 165 } 166 } 167 return remoteToLocalMapping; 168 } 169 170 /** Whether or not to abort on push failure. */ shouldAbortOnFailure()171 public boolean shouldAbortOnFailure() { 172 return mAbortOnFailure; 173 } 174 175 /** {@inheritDoc} */ 176 @Override setAbi(IAbi abi)177 public void setAbi(IAbi abi) { 178 mAbi = abi; 179 } 180 181 /** {@inheritDoc} */ 182 @Override getAbi()183 public IAbi getAbi() { 184 return mAbi; 185 } 186 187 /** {@inheritDoc} */ 188 @Override setInvocationContext(IInvocationContext invocationContext)189 public void setInvocationContext(IInvocationContext invocationContext) { 190 if (invocationContext.getAttributes().get(ModuleDefinition.MODULE_NAME) != null) { 191 // Only keep the module name 192 mModuleName = 193 invocationContext.getAttributes().get(ModuleDefinition.MODULE_NAME).get(0); 194 } 195 } 196 197 /** 198 * Resolve relative file path via {@link IBuildInfo} and test cases directories. 199 * 200 * @param buildInfo the build artifact information 201 * @param fileName relative file path to be resolved 202 * @return the file from the build info or test cases directories 203 */ resolveRelativeFilePath(IBuildInfo buildInfo, String fileName)204 public File resolveRelativeFilePath(IBuildInfo buildInfo, String fileName) { 205 File src = null; 206 if (buildInfo != null) { 207 src = buildInfo.getFile(fileName); 208 if (src != null && src.exists()) { 209 return src; 210 } 211 } 212 if (buildInfo instanceof IDeviceBuildInfo) { 213 IDeviceBuildInfo deviceBuild = (IDeviceBuildInfo) buildInfo; 214 File testDir = deviceBuild.getTestsDir(); 215 List<File> scanDirs = new ArrayList<>(); 216 // If it exists, always look first in the ANDROID_TARGET_OUT_TESTCASES 217 File targetTestCases = deviceBuild.getFile(BuildInfoFileKey.TARGET_LINKED_DIR); 218 if (targetTestCases != null) { 219 scanDirs.add(targetTestCases); 220 } 221 if (testDir != null) { 222 scanDirs.add(testDir); 223 } 224 225 if (mModuleName != null) { 226 // Use module name as a discriminant to find some files 227 if (testDir != null) { 228 try { 229 File moduleDir = 230 FileUtil.findDirectory( 231 mModuleName, scanDirs.toArray(new File[] {})); 232 if (moduleDir != null) { 233 // If the spec is pushing the module itself 234 if (mModuleName.equals(fileName)) { 235 // If that's the main binary generated by the target, we push the 236 // full directory 237 return moduleDir; 238 } 239 // Search the module directory if it exists use it in priority 240 src = FileUtil.findFile(fileName, null, moduleDir); 241 if (src != null) { 242 // Search again with filtering on ABI 243 File srcWithAbi = FileUtil.findFile(fileName, mAbi, moduleDir); 244 if (srcWithAbi != null 245 && !srcWithAbi 246 .getAbsolutePath() 247 .startsWith(src.getAbsolutePath())) { 248 // When multiple matches are found, return the one with matching 249 // ABI unless src is its parent directory. 250 return srcWithAbi; 251 } 252 return src; 253 } 254 } else { 255 CLog.d("Did not find any module directory for '%s'", mModuleName); 256 } 257 258 } catch (IOException e) { 259 CLog.w( 260 "Something went wrong while searching for the module '%s' " 261 + "directory.", 262 mModuleName); 263 } 264 } 265 } 266 // Search top-level matches 267 for (File searchDir : scanDirs) { 268 try { 269 Set<File> allMatch = FileUtil.findFilesObject(searchDir, fileName); 270 if (allMatch.size() > 1) { 271 CLog.d( 272 "Several match for filename '%s', searching for top-level match.", 273 fileName); 274 for (File f : allMatch) { 275 // Bias toward direct child / top level nodes 276 if (f.getParent().equals(searchDir.getAbsolutePath())) { 277 return f; 278 } 279 } 280 } else if (allMatch.size() == 1) { 281 return allMatch.iterator().next(); 282 } 283 } catch (IOException e) { 284 CLog.w("Failed to find test files from directory."); 285 } 286 } 287 // Fall-back to searching everything 288 try { 289 // Search the full tests dir if no target dir is available. 290 src = FileUtil.findFile(fileName, null, scanDirs.toArray(new File[] {})); 291 if (src != null) { 292 // Search again with filtering on ABI 293 File srcWithAbi = 294 FileUtil.findFile(fileName, mAbi, scanDirs.toArray(new File[] {})); 295 if (srcWithAbi != null 296 && !srcWithAbi.getAbsolutePath().startsWith(src.getAbsolutePath())) { 297 // When multiple matches are found, return the one with matching 298 // ABI unless src is its parent directory. 299 return srcWithAbi; 300 } 301 return src; 302 } 303 } catch (IOException e) { 304 CLog.w("Failed to find test files from directory."); 305 src = null; 306 } 307 308 if (src == null && testDir != null) { 309 // TODO(b/138416078): Once build dependency can be fixed and test required 310 // APKs are all under the test module directory, we can remove this fallback 311 // approach to do individual download from remote artifact. 312 // Try to stage the files from remote zip files. 313 src = buildInfo.stageRemoteFile(fileName, testDir); 314 } 315 } 316 return src; 317 } 318 319 /** {@inheritDoc} */ 320 @Override setUp(TestInformation testInfo)321 public void setUp(TestInformation testInfo) 322 throws TargetSetupError, BuildError, DeviceNotAvailableException { 323 mFilesPushed = new HashSet<>(); 324 ITestDevice device = testInfo.getDevice(); 325 if (mRemountSystem) { 326 device.remountSystemWritable(); 327 } 328 if (mRemountVendor) { 329 device.remountVendorWritable(); 330 } 331 332 // Backup files 333 for (Map.Entry<String, String> entry : mBackupFileSpecs.entrySet()) { 334 device.executeShellCommand( 335 "mv \"" + entry.getKey() + "\" \"" + entry.getValue() + "\""); 336 } 337 338 Map<String, File> remoteToLocalMapping = getPushSpecs(device.getDeviceDescriptor()); 339 for (String remotePath : remoteToLocalMapping.keySet()) { 340 File local = remoteToLocalMapping.get(remotePath); 341 Log.d( 342 LOG_TAG, 343 String.format( 344 "Trying to push local '%s' to remote '%s'", 345 local.getPath(), remotePath)); 346 evaluatePushingPair(device, testInfo.getBuildInfo(), local, remotePath); 347 } 348 349 for (String command : mPostPushCommands) { 350 device.executeShellCommand(command); 351 } 352 353 if (mTriggerMediaScan) { 354 String mountPoint = device.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE); 355 device.executeShellCommand(String.format(MEDIA_SCAN_INTENT, mountPoint)); 356 } 357 } 358 359 /** {@inheritDoc} */ 360 @Override tearDown(TestInformation testInfo, Throwable e)361 public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException { 362 ITestDevice device = testInfo.getDevice(); 363 if (!(e instanceof DeviceNotAvailableException) && mCleanup && mFilesPushed != null) { 364 if (mRemountSystem) { 365 device.remountSystemWritable(); 366 } 367 if (mRemountVendor) { 368 device.remountVendorWritable(); 369 } 370 for (String devicePath : mFilesPushed) { 371 device.deleteFile(devicePath); 372 } 373 // Restore files 374 for (Map.Entry<String, String> entry : mBackupFileSpecs.entrySet()) { 375 device.executeShellCommand( 376 "mv \"" + entry.getValue() + "\" \"" + entry.getKey() + "\""); 377 } 378 } 379 } 380 evaluatePushingPair( ITestDevice device, IBuildInfo buildInfo, File src, String remotePath)381 private void evaluatePushingPair( 382 ITestDevice device, IBuildInfo buildInfo, File src, String remotePath) 383 throws TargetSetupError, DeviceNotAvailableException { 384 String localPath = src.getPath(); 385 if (!src.isAbsolute()) { 386 src = resolveRelativeFilePath(buildInfo, localPath); 387 } 388 if (src == null || !src.exists()) { 389 fail( 390 String.format("Local source file '%s' does not exist", localPath), 391 device.getDeviceDescriptor()); 392 return; 393 } 394 if (src.isDirectory()) { 395 boolean deleteContentOnly = true; 396 if (!device.doesFileExist(remotePath)) { 397 device.executeShellCommand(String.format("mkdir -p \"%s\"", remotePath)); 398 deleteContentOnly = false; 399 } else if (!device.isDirectory(remotePath)) { 400 // File exists and is not a directory 401 throw new TargetSetupError( 402 String.format( 403 "Attempting to push dir '%s' to an existing device file '%s'", 404 src.getAbsolutePath(), remotePath), 405 device.getDeviceDescriptor()); 406 } 407 Set<String> filter = new HashSet<>(); 408 if (mAbi != null) { 409 String currentArch = AbiUtils.getArchForAbi(mAbi.getName()); 410 filter.addAll(AbiUtils.getArchSupported()); 411 filter.remove(currentArch); 412 } 413 // TODO: Look into using syncFiles but that requires improving sync to work for unroot 414 if (!device.pushDir(src, remotePath, filter)) { 415 fail( 416 String.format( 417 "Failed to push local '%s' to remote '%s'", localPath, remotePath), 418 device.getDeviceDescriptor()); 419 return; 420 } else { 421 if (deleteContentOnly) { 422 remotePath += "/*"; 423 } 424 mFilesPushed.add(remotePath); 425 } 426 } else { 427 if (!device.pushFile(src, remotePath)) { 428 fail( 429 String.format( 430 "Failed to push local '%s' to remote '%s'", localPath, remotePath), 431 device.getDeviceDescriptor()); 432 return; 433 } else { 434 mFilesPushed.add(remotePath); 435 } 436 } 437 } 438 } 439