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.tradefed.build.BuildInfoKey.BuildInfoFileKey; 21 import com.android.tradefed.build.IBuildInfo; 22 import com.android.tradefed.build.IDeviceBuildInfo; 23 import com.android.tradefed.command.remote.DeviceDescriptor; 24 import com.android.tradefed.config.Option; 25 import com.android.tradefed.config.OptionClass; 26 import com.android.tradefed.device.DeviceNotAvailableException; 27 import com.android.tradefed.device.ITestDevice; 28 import com.android.tradefed.invoker.IInvocationContext; 29 import com.android.tradefed.invoker.TestInformation; 30 import com.android.tradefed.invoker.logger.InvocationMetricLogger; 31 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey; 32 import com.android.tradefed.log.LogUtil.CLog; 33 import com.android.tradefed.observatory.IDiscoverDependencies; 34 import com.android.tradefed.result.error.DeviceErrorIdentifier; 35 import com.android.tradefed.result.error.ErrorIdentifier; 36 import com.android.tradefed.result.error.InfraErrorIdentifier; 37 import com.android.tradefed.testtype.IAbi; 38 import com.android.tradefed.testtype.IAbiReceiver; 39 import com.android.tradefed.testtype.IInvocationContextReceiver; 40 import com.android.tradefed.testtype.suite.ModuleDefinition; 41 import com.android.tradefed.util.AbiUtils; 42 import com.android.tradefed.util.FileUtil; 43 import com.android.tradefed.util.MultiMap; 44 import com.android.tradefed.util.SearchArtifactUtil; 45 46 import java.io.File; 47 import java.io.IOException; 48 import java.util.ArrayList; 49 import java.util.Arrays; 50 import java.util.Collection; 51 import java.util.HashSet; 52 import java.util.LinkedHashMap; 53 import java.util.List; 54 import java.util.Map; 55 import java.util.Set; 56 57 /** 58 * A {@link ITargetPreparer} that attempts to push any number of files from any host path to any 59 * device path. 60 * 61 * <p>Should be performed *after* a new build is flashed, and *after* DeviceSetup is run (if 62 * enabled) 63 */ 64 @OptionClass(alias = "push-file") 65 public class PushFilePreparer extends BaseTargetPreparer 66 implements IAbiReceiver, IInvocationContextReceiver, IDiscoverDependencies { 67 private static final String MEDIA_SCAN_INTENT = 68 "am broadcast -a android.intent.action.MEDIA_MOUNTED -d file://%s " 69 + "--receiver-include-background"; 70 71 private IAbi mAbi; 72 73 @Deprecated 74 @Option( 75 name = "push", 76 description = 77 "Deprecated. Please use push-file instead. A push-spec, formatted as " 78 + "'/localpath/to/srcfile.txt->/devicepath/to/destfile.txt' " 79 + "or '/localpath/to/srcfile.txt->/devicepath/to/destdir/'. " 80 + "May be repeated. The local path may be relative to the test cases " 81 + "build out directories " 82 + "($ANDROID_HOST_OUT_TESTCASES / $ANDROID_TARGET_OUT_TESTCASES)." 83 ) 84 private Collection<String> mPushSpecs = new ArrayList<>(); 85 86 @Option( 87 name = "push-file", 88 description = 89 "A push-spec, specifying the local file to the path where it should be pushed" 90 + " on device. May be repeated. If multiple files are configured to be" 91 + " pushed to the same remote path, the latest one will be pushed.") 92 private MultiMap<File, String> mPushFileSpecs = new MultiMap<>(); 93 94 @Option( 95 name = "skip-abi-filtering", 96 description = 97 "A bool to indicate we should or shouldn't skip files that match the " 98 + "architecture string name, e.g. x86, x86_64, arm64-v8. This " 99 + "is necessary when file or folder names match an architecture " 100 + "version but still need to be pushed to the device.") 101 private boolean mSkipAbiFiltering = false; 102 103 @Option( 104 name = "backup-file", 105 description = 106 "A key/value pair, the with key specifying a device file path to be backed up, " 107 + "and the value a device file path indicating where to save the file. " 108 + "During tear-down, the values will be executed in reverse, " 109 + "restoring the backup file location to the initial location. " 110 + "May be repeated.") 111 private Map<String, String> mBackupFileSpecs = new LinkedHashMap<>(); 112 113 @Option(name="post-push", description= 114 "A command to run on the device (with `adb shell (yourcommand)`) after all pushes " + 115 "have been attempted. Will not be run if a push fails with abort-on-push-failure " + 116 "enabled. May be repeated.") 117 private Collection<String> mPostPushCommands = new ArrayList<>(); 118 119 @Option(name="abort-on-push-failure", description= 120 "If false, continue if pushes fail. If true, abort the Invocation on any failure.") 121 private boolean mAbortOnFailure = true; 122 123 @Option(name="trigger-media-scan", description= 124 "After pushing files, trigger a media scan of external storage on device.") 125 private boolean mTriggerMediaScan = false; 126 127 @Option( 128 name = "cleanup", 129 description = 130 "Whether files pushed onto device should be cleaned up after test. Note that" 131 + " the preparer does not verify that files/directories have been deleted.") 132 private boolean mCleanup = true; 133 134 @Option( 135 name = "remount-system", 136 description = 137 "Remounts system partition to be writable " 138 + "so that files could be pushed there too") 139 private boolean mRemountSystem = false; 140 141 @Option( 142 name = "remount-vendor", 143 description = 144 "Remounts vendor partition to be writable " 145 + "so that files could be pushed there too") 146 private boolean mRemountVendor = false; 147 148 private Set<String> mFilesPushed = null; 149 /** If the preparer is part of a module, we can use the test module name as a search criteria */ 150 private String mModuleName = null; 151 152 /** 153 * Helper method to only throw if mAbortOnFailure is enabled. Callers should behave as if this 154 * method may return. 155 */ fail(String message, DeviceDescriptor descriptor, ErrorIdentifier identifier)156 private void fail(String message, DeviceDescriptor descriptor, ErrorIdentifier identifier) 157 throws TargetSetupError { 158 if (shouldAbortOnFailure()) { 159 throw new TargetSetupError(message, descriptor, identifier); 160 } else { 161 // Log the error and return 162 CLog.w(message); 163 } 164 } 165 166 /** Create the list of files to be pushed. */ getPushSpecs(ITestDevice device)167 public final Map<String, File> getPushSpecs(ITestDevice device) throws TargetSetupError { 168 Map<String, File> remoteToLocalMapping = new LinkedHashMap<>(); 169 for (String pushspec : mPushSpecs) { 170 String[] pair = pushspec.split("->"); 171 if (pair.length != 2) { 172 fail( 173 String.format("Invalid pushspec: '%s'", Arrays.asList(pair)), 174 device.getDeviceDescriptor(), 175 InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR); 176 continue; 177 } 178 remoteToLocalMapping.put(pair[1], new File(pair[0])); 179 } 180 // Push the file structure 181 for (File local : mPushFileSpecs.keySet()) { 182 for (String remoteLocation : mPushFileSpecs.get(local)) { 183 remoteToLocalMapping.put(remoteLocation, local); 184 } 185 } 186 return remoteToLocalMapping; 187 } 188 189 /** Whether or not to abort on push failure. */ shouldAbortOnFailure()190 public boolean shouldAbortOnFailure() { 191 return mAbortOnFailure; 192 } 193 194 /** {@inheritDoc} */ 195 @Override setAbi(IAbi abi)196 public void setAbi(IAbi abi) { 197 mAbi = abi; 198 } 199 200 /** {@inheritDoc} */ 201 @Override getAbi()202 public IAbi getAbi() { 203 return mAbi; 204 } 205 206 /** {@inheritDoc} */ 207 @Override setInvocationContext(IInvocationContext invocationContext)208 public void setInvocationContext(IInvocationContext invocationContext) { 209 if (invocationContext.getAttributes().get(ModuleDefinition.MODULE_NAME) != null) { 210 // Only keep the module name 211 mModuleName = 212 invocationContext.getAttributes().get(ModuleDefinition.MODULE_NAME).get(0); 213 } 214 } 215 216 /** 217 * Resolve relative file path via {@link IBuildInfo} and test cases directories. 218 * 219 * @param buildInfo the build artifact information 220 * @param fileName relative file path to be resolved 221 * @return the file from the build info or test cases directories 222 */ resolveRelativeFilePath(IBuildInfo buildInfo, String fileName)223 public File resolveRelativeFilePath(IBuildInfo buildInfo, String fileName) { 224 File src = null; 225 try { 226 src = SearchArtifactUtil.searchFile(fileName, true, mAbi, null, null, null, true); 227 } catch (Exception e) { 228 // TODO: handle error when migration is complete. 229 CLog.e(e); 230 } 231 if (src != null && src.exists()) { 232 return src; 233 } else { 234 // Silently report not found and fall back to old logic. 235 InvocationMetricLogger.addInvocationMetrics( 236 InvocationMetricKey.SEARCH_ARTIFACT_FAILURE_COUNT, 1); 237 } 238 if (buildInfo != null) { 239 src = buildInfo.getFile(fileName); 240 if (src != null && src.exists()) { 241 return src; 242 } 243 } 244 if (buildInfo instanceof IDeviceBuildInfo) { 245 IDeviceBuildInfo deviceBuild = (IDeviceBuildInfo) buildInfo; 246 File testDir = deviceBuild.getTestsDir(); 247 List<File> scanDirs = new ArrayList<>(); 248 // If it exists, always look first in the ANDROID_TARGET_OUT_TESTCASES 249 File targetTestCases = deviceBuild.getFile(BuildInfoFileKey.TARGET_LINKED_DIR); 250 if (targetTestCases != null) { 251 scanDirs.add(targetTestCases); 252 } 253 if (testDir != null) { 254 scanDirs.add(testDir); 255 } 256 257 if (mModuleName != null) { 258 // Use module name as a discriminant to find some files 259 if (testDir != null) { 260 try { 261 File moduleDir = 262 FileUtil.findDirectory( 263 mModuleName, scanDirs.toArray(new File[] {})); 264 if (moduleDir == null) { 265 moduleDir = SearchArtifactUtil.getModuleDirFromConfig(); 266 } 267 if (moduleDir != null) { 268 // If the spec is pushing the module itself 269 if (mModuleName.equals(fileName)) { 270 // If that's the main binary generated by the target, we push the 271 // full directory 272 return moduleDir; 273 } 274 // Search the module directory if it exists use it in priority 275 src = FileUtil.findFile(fileName, null, moduleDir); 276 if (src != null) { 277 // Search again with filtering on ABI 278 File srcWithAbi = FileUtil.findFile(fileName, mAbi, moduleDir); 279 if (srcWithAbi != null 280 && !srcWithAbi 281 .getAbsolutePath() 282 .startsWith(src.getAbsolutePath())) { 283 // When multiple matches are found, return the one with matching 284 // ABI unless src is its parent directory. 285 return srcWithAbi; 286 } 287 return src; 288 } 289 } else { 290 CLog.d("Did not find any module directory for '%s'", mModuleName); 291 } 292 293 } catch (IOException e) { 294 CLog.w( 295 "Something went wrong while searching for the module '%s' " 296 + "directory.", 297 mModuleName); 298 } 299 } 300 } 301 // Search top-level matches 302 for (File searchDir : scanDirs) { 303 try { 304 Set<File> allMatch = FileUtil.findFilesObject(searchDir, fileName); 305 if (allMatch.size() > 1) { 306 CLog.d( 307 "Several match for filename '%s', searching for top-level match.", 308 fileName); 309 for (File f : allMatch) { 310 // Bias toward direct child / top level nodes 311 if (f.getParent().equals(searchDir.getAbsolutePath())) { 312 return f; 313 } 314 } 315 } else if (allMatch.size() == 1) { 316 return allMatch.iterator().next(); 317 } 318 } catch (IOException e) { 319 CLog.w("Failed to find test files from directory."); 320 } 321 } 322 // Fall-back to searching everything 323 try { 324 // Search the full tests dir if no target dir is available. 325 src = FileUtil.findFile(fileName, null, scanDirs.toArray(new File[] {})); 326 if (src != null) { 327 // Search again with filtering on ABI 328 File srcWithAbi = 329 FileUtil.findFile(fileName, mAbi, scanDirs.toArray(new File[] {})); 330 if (srcWithAbi != null 331 && !srcWithAbi.getAbsolutePath().startsWith(src.getAbsolutePath())) { 332 // When multiple matches are found, return the one with matching 333 // ABI unless src is its parent directory. 334 return srcWithAbi; 335 } 336 return src; 337 } 338 } catch (IOException e) { 339 CLog.w("Failed to find test files from directory."); 340 src = null; 341 } 342 343 if (src == null && testDir != null) { 344 // TODO(b/138416078): Once build dependency can be fixed and test required 345 // APKs are all under the test module directory, we can remove this fallback 346 // approach to do individual download from remote artifact. 347 // Try to stage the files from remote zip files. 348 src = buildInfo.stageRemoteFile(fileName, testDir); 349 if (src != null) { 350 InvocationMetricLogger.addInvocationMetrics( 351 InvocationMetricKey.STAGE_UNDEFINED_DEPENDENCY, fileName); 352 try { 353 // Search again with filtering on ABI 354 File srcWithAbi = FileUtil.findFile(fileName, mAbi, testDir); 355 if (srcWithAbi != null 356 && !srcWithAbi 357 .getAbsolutePath() 358 .startsWith(src.getAbsolutePath())) { 359 // When multiple matches are found, return the one with matching 360 // ABI unless src is its parent directory. 361 return srcWithAbi; 362 } 363 } catch (IOException e) { 364 CLog.w("Failed to find test files with matching ABI from directory."); 365 } 366 } 367 } 368 } 369 if (src == null) { 370 // if old logic fails too, do not report search artifact failure 371 InvocationMetricLogger.addInvocationMetrics( 372 InvocationMetricKey.SEARCH_ARTIFACT_FAILURE_COUNT, -1); 373 } 374 return src; 375 } 376 377 /** {@inheritDoc} */ 378 @Override setUp(TestInformation testInfo)379 public void setUp(TestInformation testInfo) 380 throws TargetSetupError, BuildError, DeviceNotAvailableException { 381 mFilesPushed = new HashSet<>(); 382 ITestDevice device = testInfo.getDevice(); 383 if (mRemountSystem) { 384 device.remountSystemWritable(); 385 } 386 if (mRemountVendor) { 387 device.remountVendorWritable(); 388 } 389 390 // Backup files 391 for (Map.Entry<String, String> entry : mBackupFileSpecs.entrySet()) { 392 device.executeShellCommand( 393 "mv \"" + entry.getKey() + "\" \"" + entry.getValue() + "\""); 394 } 395 396 Map<String, File> remoteToLocalMapping = getPushSpecs(device); 397 for (String remotePath : remoteToLocalMapping.keySet()) { 398 File local = remoteToLocalMapping.get(remotePath); 399 CLog.d("Trying to push local '%s' to remote '%s'", local.getPath(), remotePath); 400 evaluatePushingPair(device, testInfo.getBuildInfo(), local, remotePath); 401 } 402 403 for (String command : mPostPushCommands) { 404 device.executeShellCommand(command); 405 } 406 407 if (mTriggerMediaScan) { 408 String mountPoint = device.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE); 409 device.executeShellCommand(String.format(MEDIA_SCAN_INTENT, mountPoint)); 410 } 411 } 412 413 /** {@inheritDoc} */ 414 @Override tearDown(TestInformation testInfo, Throwable e)415 public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException { 416 ITestDevice device = testInfo.getDevice(); 417 if (!(e instanceof DeviceNotAvailableException) && mCleanup && mFilesPushed != null) { 418 if (mRemountSystem) { 419 device.remountSystemReadOnly(); 420 } 421 if (mRemountVendor) { 422 device.remountVendorReadOnly(); 423 } 424 for (String devicePath : mFilesPushed) { 425 device.deleteFile(devicePath); 426 } 427 // Restore files 428 for (Map.Entry<String, String> entry : mBackupFileSpecs.entrySet()) { 429 device.executeShellCommand( 430 "mv \"" + entry.getValue() + "\" \"" + entry.getKey() + "\""); 431 } 432 } 433 } 434 evaluatePushingPair( ITestDevice device, IBuildInfo buildInfo, File src, String remotePath)435 private void evaluatePushingPair( 436 ITestDevice device, IBuildInfo buildInfo, File src, String remotePath) 437 throws TargetSetupError, DeviceNotAvailableException { 438 String localPath = src.getPath(); 439 if (!src.isAbsolute()) { 440 src = resolveRelativeFilePath(buildInfo, localPath); 441 } 442 if (src == null || !src.exists()) { 443 fail( 444 String.format("Local source file '%s' does not exist", localPath), 445 device.getDeviceDescriptor(), 446 InfraErrorIdentifier.CONFIGURED_ARTIFACT_NOT_FOUND); 447 return; 448 } 449 if (src.isDirectory()) { 450 boolean deleteContentOnly = true; 451 if (!device.doesFileExist(remotePath)) { 452 device.executeShellCommand(String.format("mkdir -p \"%s\"", remotePath)); 453 deleteContentOnly = false; 454 } else if (!device.isDirectory(remotePath)) { 455 // File exists and is not a directory 456 throw new TargetSetupError( 457 String.format( 458 "Attempting to push dir '%s' to an existing device file '%s'", 459 src.getAbsolutePath(), remotePath), 460 device.getDeviceDescriptor(), 461 DeviceErrorIdentifier.FAIL_PUSH_FILE); 462 } 463 Set<String> filter = new HashSet<>(); 464 if (mAbi != null && !mSkipAbiFiltering) { 465 String currentArch = AbiUtils.getArchForAbi(mAbi.getName()); 466 filter.addAll(AbiUtils.getArchSupported()); 467 filter.remove(currentArch); 468 } 469 // TODO: Look into using syncFiles but that requires improving sync to work for unroot 470 if (!device.pushDir(src, remotePath, filter)) { 471 fail( 472 String.format( 473 "Failed to push local '%s' to remote '%s'", localPath, remotePath), 474 device.getDeviceDescriptor(), 475 DeviceErrorIdentifier.FAIL_PUSH_FILE); 476 return; 477 } else { 478 if (deleteContentOnly) { 479 remotePath += "/*"; 480 } 481 mFilesPushed.add(remotePath); 482 } 483 } else { 484 if (!device.pushFile(src, remotePath)) { 485 fail( 486 String.format( 487 "Failed to push local '%s' to remote '%s'", localPath, remotePath), 488 device.getDeviceDescriptor(), 489 DeviceErrorIdentifier.FAIL_PUSH_FILE); 490 return; 491 } else { 492 mFilesPushed.add(remotePath); 493 } 494 } 495 } 496 497 @Override reportDependencies()498 public Set<String> reportDependencies() { 499 Set<String> deps = new HashSet<>(); 500 try { 501 for (File f : getPushSpecs(null).values()) { 502 // Match the resolving logic when actually pushing 503 if (!f.isAbsolute()) { 504 deps.add(f.getName()); 505 } else { 506 CLog.d( 507 "%s detected as existing. Not reported as dependency.", 508 f.getAbsolutePath()); 509 } 510 } 511 } catch (TargetSetupError e) { 512 CLog.e(e); 513 } 514 return deps; 515 } 516 shouldRemountSystem()517 public boolean shouldRemountSystem() { 518 return mRemountSystem; 519 } 520 shouldRemountVendor()521 public boolean shouldRemountVendor() { 522 return mRemountVendor; 523 } 524 isCleanUpEnabled()525 public boolean isCleanUpEnabled() { 526 return mCleanup; 527 } 528 } 529