• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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