1 /*
2  * Copyright (C) 2012 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.testtype;
18 
19 import com.android.ddmlib.FileListingService;
20 import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
21 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
22 import com.android.tradefed.config.Option;
23 import com.android.tradefed.device.DeviceNotAvailableException;
24 import com.android.tradefed.device.IFileEntry;
25 import com.android.tradefed.device.ITestDevice;
26 import com.android.tradefed.invoker.TestInformation;
27 import com.android.tradefed.log.LogUtil.CLog;
28 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
29 import com.android.tradefed.result.FileInputStreamSource;
30 import com.android.tradefed.result.ITestInvocationListener;
31 import com.android.tradefed.result.ITestLifeCycleReceiver;
32 import com.android.tradefed.result.InputStreamSource;
33 import com.android.tradefed.result.LogDataType;
34 import com.android.tradefed.result.TestDescription;
35 import com.android.tradefed.util.FileUtil;
36 import com.android.tradefed.util.IRunUtil;
37 import com.android.tradefed.util.RunUtil;
38 import com.android.tradefed.util.StreamUtil;
39 import com.android.tradefed.util.ZipUtil;
40 
41 import org.junit.Assert;
42 
43 import java.io.File;
44 import java.io.IOException;
45 import java.util.ArrayList;
46 import java.util.Collection;
47 import java.util.HashMap;
48 import java.util.LinkedHashMap;
49 import java.util.LinkedHashSet;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.Set;
53 import java.util.concurrent.TimeUnit;
54 
55 public class UiAutomatorTest implements IRemoteTest, IDeviceTest, ITestFilterReceiver {
56 
57     public enum LoggingOption {
58         AFTER_TEST,
59         AFTER_FAILURE,
60         OFF,
61     }
62 
63     public enum TestFailureAction {
64         BUGREPORT,
65         SCREENSHOT,
66         BUGREPORT_AND_SCREENSHOT,
67     }
68 
69     private static final String SHELL_EXE_BASE = "/data/local/tmp/";
70     private static final String TRACE_ITERATIONS = "traceIterations";
71     private static final String TRACE_DEST_DIRECTORY = "destDirectory";
72 
73     private ITestDevice mDevice = null;
74     private IRemoteAndroidTestRunner mRunner = null;
75     protected Collection<ITestLifeCycleReceiver> mListeners = new ArrayList<>();
76 
77     @Option(name = "jar-path", description = "path to jars containing UI Automator test cases and"
78             + " dependencies; May be repeated. " +
79             "If unspecified will use all jars found in /data/local/tmp/")
80     private List<String> mJarPaths = new ArrayList<String>();
81 
82     @Option(
83         name = "class",
84         description =
85                 "test class to run, may be repeated; multiple classess will be run"
86                         + " in the same order as provided in command line"
87     )
88     private Set<String> mClasses = new LinkedHashSet<>();
89 
90     @Option(name = "sync-time", description = "time to allow for initial sync, in ms")
91     private long mSyncTime = 0;
92 
93     @Option(name = "run-arg",
94             description = "Additional test specific arguments to provide.")
95     private Map<String, String> mArgMap = new LinkedHashMap<String, String>();
96 
97     @Option(name = "timeout",
98             description = "Aborts the test run if any test takes longer than the specified "
99                     + "timeout. For no timeout, set to 0.", isTimeVal = true)
100     private long mTestTimeout = 30 * 60 * 1000; // default to 30 minutes
101 
102     @Option(name = "capture-logs", description =
103             "capture bugreport and screenshot as specified.")
104     private LoggingOption mLoggingOption = LoggingOption.AFTER_FAILURE;
105 
106     @Option(name = "runner-path", description = "path to uiautomator runner; may be null and "
107             + "default will be used in this case")
108     private String mRunnerPath = null;
109 
110     @Option(name = "on-test-failure",
111             description = "sets the action to perform if a test fails")
112     private TestFailureAction mFailureAction = TestFailureAction.BUGREPORT_AND_SCREENSHOT;
113 
114     @Option(name = "ignore-sighup",
115             description = "allows uiautomator test to ignore SIGHUP signal")
116     private boolean mIgnoreSighup = false;
117 
118     @Option(name = "run-name",
119             description = "the run name to use when reporting test results.")
120     private String mRunName = "uiautomator";
121 
122     @Option(name = "instrumentation",
123             description = "the specified test should be driven with instrumentation."
124             + "jar-path, runner-path, ignore-sighup are ignored when this is set.")
125     private boolean mInstrumentation = false;
126 
127     @Option(name = "package",
128             description = "The manifest package name of the UI test package."
129             + "Only applies when 'instrumentation' option is set.")
130     private String mPackage = null;
131 
132     @Option(name = "runner",
133             description="The instrumentation based test runner class name to use."
134             + "Only applies when 'instrumentation' option is set.")
135     private String mRunnerName =
136         "android.support.test.uiautomator.UiAutomatorInstrumentationTestRunner";
137 
138     @Option(
139         name = "hidden-api-checks",
140         description =
141                 "If set to false, the '--no-hidden-api-checks' flag will be passed to the am "
142                         + "instrument command. Only works for P or later."
143     )
144     private boolean mHiddenApiChecks = true;
145 
146     @Option(
147             name = "test-api-access",
148             description =
149                     "If set to false and hidden API checks are enabled, the '--no-test-api-access'"
150                             + " flag will be passed to the am instrument command."
151                             + " Only works for R or later.")
152     private boolean mTestApiAccess = true;
153 
154     @Option(
155         name = "isolated-storage",
156         description =
157                 "If set to false, the '--no-isolated-storage' flag will be passed to the am "
158                         + "instrument command. Only works for Q or later."
159     )
160     private boolean mIsolatedStorage = true;
161 
162     @Option(
163         name = "window-animation",
164         description =
165                 "If set to false, the '--no-window-animation' flag will be passed to the am "
166                         + "instrument command. Only works for ICS or later."
167     )
168     private boolean mWindowAnimation = true;
169 
170     /**
171      * {@inheritDoc}
172      */
173     @Override
setDevice(ITestDevice device)174     public void setDevice(ITestDevice device) {
175         mDevice = device;
176     }
177 
178     /**
179      * {@inheritDoc}
180      */
181     @Override
getDevice()182     public ITestDevice getDevice() {
183         return mDevice;
184     }
185 
setLoggingOption(LoggingOption loggingOption)186     public void setLoggingOption(LoggingOption loggingOption) {
187         mLoggingOption = loggingOption;
188     }
189 
190     /**
191      * @deprecated use {@link #setLoggingOption(LoggingOption)} instead.
192      * <p/>
193      * Retained for compatibility with cts-tradefed
194      */
195     @Deprecated
setCaptureLogs(boolean captureLogs)196     public void setCaptureLogs(boolean captureLogs) {
197         if (captureLogs) {
198             setLoggingOption(LoggingOption.AFTER_FAILURE);
199         } else {
200             setLoggingOption(LoggingOption.OFF);
201         }
202     }
203 
setRunName(String runName)204     public void setRunName(String runName) {
205         mRunName = runName;
206     }
207 
208     /** {@inheritDoc} */
209     @Override
run(TestInformation testInfo, ITestInvocationListener listener)210     public void run(TestInformation testInfo, ITestInvocationListener listener)
211             throws DeviceNotAvailableException {
212         mListeners.add(listener);
213         if (!isInstrumentationTest()) {
214             buildJarPaths();
215         }
216         mRunner = createTestRunner();
217         if (!mClasses.isEmpty()) {
218             getTestRunner().setClassNames(mClasses.toArray(new String[]{}));
219         }
220         getTestRunner().setRunName(mRunName);
221         preTestSetup();
222         getRunUtil().sleep(getSyncTime());
223         getTestRunner().setMaxTimeToOutputResponse(mTestTimeout, TimeUnit.MILLISECONDS);
224         for (Map.Entry<String, String> entry : getTestRunArgMap().entrySet()) {
225             getTestRunner().addInstrumentationArg(entry.getKey(), entry.getValue());
226         }
227         if (!isInstrumentationTest()) {
228             ((UiAutomatorRunner)getTestRunner()).setIgnoreSighup(mIgnoreSighup);
229         }
230         if (mLoggingOption != LoggingOption.OFF) {
231             mListeners.add(new LoggingWrapper(listener));
232             getDevice().runInstrumentationTests(getTestRunner(), mListeners);
233         } else {
234             getDevice().runInstrumentationTests(getTestRunner(), mListeners);
235         }
236 
237         if (getTestRunArgMap().containsKey(TRACE_ITERATIONS) &&
238                 getTestRunArgMap().containsKey(TRACE_DEST_DIRECTORY)) {
239             try {
240                 logTraceFiles(listener, getTestRunArgMap().get(TRACE_DEST_DIRECTORY));
241             } catch (IOException e) {
242                 CLog.e(e);
243             }
244         }
245 
246     }
247 
createTestRunner()248     protected IRemoteAndroidTestRunner createTestRunner() throws DeviceNotAvailableException {
249         if (isInstrumentationTest()) {
250             if (mPackage == null) {
251                 throw new IllegalArgumentException("package name has not been set");
252             }
253             RemoteAndroidTestRunner runner =
254                     new RemoteAndroidTestRunner(mPackage, mRunnerName, getDevice().getIDevice());
255             String runOptions = "";
256             // hidden-api-checks flag only exists in P and after.
257             // Using a temp variable to consolidate the dynamic checks
258             int apiLevel = !mHiddenApiChecks || !mWindowAnimation ? getDevice().getApiLevel() : 0;
259             if (!mHiddenApiChecks && apiLevel >= 28) {
260                 runOptions += "--no-hidden-api-checks ";
261             }
262             // test-api-access flag only exists in R and after.
263             // Test API checks are subset of hidden API checks, so only make sense if hidden API
264             // checks are enabled.
265             if (mHiddenApiChecks
266                     && !mTestApiAccess
267                     && getDevice().checkApiLevelAgainstNextRelease(30)) {
268                 runOptions += "--no-test-api-access ";
269             }
270             // isolated-storage flag only exists in Q and after.
271             if (!mIsolatedStorage && getDevice().checkApiLevelAgainstNextRelease(29)) {
272                 runOptions += "--no-isolated-storage ";
273             }
274             // window-animation flag only exists in ICS and after.
275             if (!mWindowAnimation && apiLevel >= 14) {
276                 runOptions += "--no-window-animation ";
277             }
278 
279             // Set the run options if any.
280             if (!runOptions.isEmpty()) {
281                 runner.setRunOptions(runOptions);
282             }
283 
284             return runner;
285         } else {
286             return new UiAutomatorRunner(getDevice().getIDevice(),
287                     getTestJarPaths().toArray(new String[]{}), mRunnerPath);
288         }
289     }
290 
buildJarPaths()291     private void buildJarPaths() throws DeviceNotAvailableException {
292         if (mJarPaths.isEmpty()) {
293             String rawFileString =
294                     getDevice().executeShellCommand(String.format("ls %s", SHELL_EXE_BASE));
295             String[] rawFiles = rawFileString.split("\r?\n");
296             for (String rawFile : rawFiles) {
297                 if (rawFile.endsWith(".jar")) {
298                     mJarPaths.add(rawFile);
299                 }
300             }
301             Assert.assertFalse(String.format("could not find jars in %s", SHELL_EXE_BASE),
302                     mJarPaths.isEmpty());
303             CLog.d("built jar paths %s", mJarPaths);
304         }
305     }
306 
307     /**
308      * Add an argument to provide when running the UI Automator tests
309      *
310      * @param key the argument name
311      * @param value the argument value
312      */
addRunArg(String key, String value)313     public void addRunArg(String key, String value) {
314         getTestRunArgMap().put(key, value);
315     }
316 
317     /**
318      * Checks if the UI Automator components are present on device
319      *
320      * @throws DeviceNotAvailableException
321      */
preTestSetup()322     protected void preTestSetup() throws DeviceNotAvailableException {
323         if (!isInstrumentationTest()) {
324             String runnerPath = ((UiAutomatorRunner)getTestRunner()).getRunnerPath();
325             if (!getDevice().doesFileExist(runnerPath)) {
326                 throw new RuntimeException("Missing UI Automator runner: " + runnerPath);
327             }
328             for (String jarPath : getTestJarPaths()) {
329                 if (!jarPath.startsWith(FileListingService.FILE_SEPARATOR)) {
330                     jarPath = SHELL_EXE_BASE + jarPath;
331                 }
332                 if (!getDevice().doesFileExist(jarPath)) {
333                     throw new RuntimeException("Missing UI Automator test jar on device: "
334                             + jarPath);
335                 }
336             }
337         }
338     }
339 
onScreenshotAndBugreport(ITestDevice device, ITestInvocationListener listener, String prefix)340     protected void onScreenshotAndBugreport(ITestDevice device, ITestInvocationListener listener,
341             String prefix) {
342         onScreenshotAndBugreport(device, listener, prefix, null);
343     }
344 
onScreenshotAndBugreport(ITestDevice device, ITestInvocationListener listener, String prefix, TestFailureAction overrideAction)345     protected void onScreenshotAndBugreport(ITestDevice device, ITestInvocationListener listener,
346             String prefix, TestFailureAction overrideAction) {
347         if (overrideAction == null) {
348             overrideAction = mFailureAction;
349         }
350         // get screen shot
351         if (overrideAction == TestFailureAction.SCREENSHOT ||
352                 overrideAction == TestFailureAction.BUGREPORT_AND_SCREENSHOT) {
353             InputStreamSource screenshot = null;
354             try {
355                 screenshot = device.getScreenshot();
356                 listener.testLog(prefix + "_screenshot", LogDataType.PNG, screenshot);
357             } catch (DeviceNotAvailableException e) {
358                 CLog.e(e);
359             } finally {
360                 StreamUtil.cancel(screenshot);
361             }
362         }
363         // get bugreport
364         if (overrideAction == TestFailureAction.BUGREPORT ||
365                 overrideAction == TestFailureAction.BUGREPORT_AND_SCREENSHOT) {
366             InputStreamSource data = null;
367             data = device.getBugreport();
368             listener.testLog(prefix + "_bugreport", LogDataType.BUGREPORT, data);
369             StreamUtil.cancel(data);
370         }
371     }
372 
373 
374     /**
375      * Pull the atrace files if they exist under traceSrcDirectory and log it
376      * @param listener test result listener
377      * @param traceSrcDirectory source directory in the device where the trace files
378      *                          are copied to the local tmp directory
379      * @throws DeviceNotAvailableException
380      * @throws IOException
381      */
logTraceFiles(ITestInvocationListener listener, String traceSrcDirectory)382     private void logTraceFiles(ITestInvocationListener listener, String traceSrcDirectory)
383             throws DeviceNotAvailableException, IOException {
384         File tmpDestDir = null;
385         try {
386             tmpDestDir = FileUtil.createTempDir("atrace");
387             IFileEntry traceSrcDir = mDevice.getFileEntry(traceSrcDirectory);
388             // Trace files are retrieved from traceSrcDirectory/testDirectory in device
389             if (traceSrcDir != null) {
390                 for (IFileEntry testDirectory : traceSrcDir.getChildren(false)) {
391                     File testTmpDirectory = new File(tmpDestDir, testDirectory.getName());
392                     if (!testTmpDirectory.mkdir()) {
393                         throw new IOException("Not able to create the atrace test directory");
394                     }
395                     for (IFileEntry traceFile : testDirectory.getChildren(false)) {
396                         File pulledFile = new File(testTmpDirectory, traceFile.getName());
397                         if (!mDevice.pullFile(traceFile.getFullPath(), pulledFile)) {
398                             throw new IOException(
399                                     "Not able to pull the trace file from test device");
400                         }
401                     }
402                     File atraceZip = ZipUtil.createZip(testTmpDirectory);
403                     try (FileInputStreamSource streamSource =
404                             new FileInputStreamSource(atraceZip)) {
405                         listener.testLog(String.format("atrace_%s", testTmpDirectory.getName()),
406                                 LogDataType.ZIP, streamSource);
407                     } finally {
408                         if (atraceZip != null) {
409                             atraceZip.delete();
410                         }
411                     }
412                 }
413             }
414         } finally {
415             if (tmpDestDir != null) {
416                 FileUtil.recursiveDelete(tmpDestDir);
417             }
418         }
419     }
420 
421     /**
422      * Wraps an existing listener, capture some data in case of test failure
423      */
424     // TODO replace this once we have a generic event triggered reporter like
425     // BugReportCollector
426     private class LoggingWrapper implements ITestInvocationListener {
427 
428         ITestInvocationListener mListener;
429         private boolean mLoggedTestFailure = false;
430         private boolean mLoggedTestRunFailure = false;
431 
LoggingWrapper(ITestInvocationListener listener)432         public LoggingWrapper(ITestInvocationListener listener) {
433             mListener = listener;
434         }
435 
436         @Override
testFailed(TestDescription test, String trace)437         public void testFailed(TestDescription test, String trace) {
438             captureFailureLog(test);
439         }
440 
441         @Override
testAssumptionFailure(TestDescription test, String trace)442         public void testAssumptionFailure(TestDescription test, String trace) {
443             captureFailureLog(test);
444         }
445 
captureFailureLog(TestDescription test)446         private void captureFailureLog(TestDescription test) {
447             if (mLoggingOption == LoggingOption.AFTER_FAILURE) {
448                 onScreenshotAndBugreport(getDevice(), mListener, String.format("%s_%s_failure",
449                         test.getClassName(), test.getTestName()));
450                 // set the flag so that we don't log again when test finishes
451                 mLoggedTestFailure = true;
452             }
453         }
454 
455         @Override
testRunFailed(String errorMessage)456         public void testRunFailed(String errorMessage) {
457             if (mLoggingOption == LoggingOption.AFTER_FAILURE) {
458                 onScreenshotAndBugreport(getDevice(), mListener, "test_run_failure");
459                 // set the flag so that we don't log again when test run finishes
460                 mLoggedTestRunFailure = true;
461             }
462         }
463 
464         @Override
testEnded(TestDescription test, HashMap<String, Metric> testMetrics)465         public void testEnded(TestDescription test, HashMap<String, Metric> testMetrics) {
466             if (!mLoggedTestFailure && mLoggingOption == LoggingOption.AFTER_TEST) {
467                 onScreenshotAndBugreport(getDevice(), mListener, String.format("%s_%s_final",
468                         test.getClassName(), test.getTestName()));
469             }
470         }
471 
472         @Override
testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics)473         public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
474             if (!mLoggedTestRunFailure && mLoggingOption == LoggingOption.AFTER_TEST) {
475                 onScreenshotAndBugreport(getDevice(), mListener, "test_run_final");
476             }
477         }
478     }
479 
getRunUtil()480     protected IRunUtil getRunUtil() {
481         return RunUtil.getDefault();
482     }
483 
484     /**
485      * @return the time allocated for the tests to sync.
486      */
getSyncTime()487     public long getSyncTime() {
488         return mSyncTime;
489     }
490 
491     /**
492      * @param syncTime the time for the tests files to sync.
493      */
setSyncTime(long syncTime)494     public void setSyncTime(long syncTime) {
495         mSyncTime = syncTime;
496     }
497 
498     /**
499      * @return the test runner.
500      */
getTestRunner()501     public IRemoteAndroidTestRunner getTestRunner() {
502         return mRunner;
503     }
504 
505     /**
506      * @return the test jar path.
507      */
getTestJarPaths()508     public List<String> getTestJarPaths() {
509         return mJarPaths;
510     }
511 
512     /**
513      * @param jarPaths the locations of the test jars.
514      */
setTestJarPaths(List<String> jarPaths)515     public void setTestJarPaths(List<String> jarPaths) {
516         mJarPaths = jarPaths;
517     }
518 
519     /**
520      * @return the arguments map to pass to the UiAutomatorRunner.
521      */
getTestRunArgMap()522     public Map<String, String> getTestRunArgMap() {
523         return mArgMap;
524     }
525 
526     /**
527      * @param runArgMap the arguments to pass to the UiAutomatorRunner.
528      */
setTestRunArgMap(Map<String, String> runArgMap)529     public void setTestRunArgMap(Map<String, String> runArgMap) {
530         mArgMap = runArgMap;
531     }
532 
533     /**
534      * Add a test class name to run.
535      */
addClassName(String className)536     public void addClassName(String className) {
537         mClasses.add(className);
538     }
539 
540     /**
541      * Add a test class name collection to run.
542      */
addClassNames(Collection<String> classNames)543     public void addClassNames(Collection<String> classNames) {
544         mClasses.addAll(classNames);
545     }
546 
isInstrumentationTest()547     public boolean isInstrumentationTest() {
548         return mInstrumentation;
549     }
550 
setRunnerName(String runnerName)551     public void setRunnerName(String runnerName) {
552         mRunnerName = runnerName;
553     }
554 
555     /**
556      * Gets the list of test class names that the harness is configured to run
557      * @return list of test class names
558      */
getClassNames()559     public List<String> getClassNames() {
560         return new ArrayList<>(mClasses);
561     }
562 
563     @Override
addIncludeFilter(String filter)564     public void addIncludeFilter(String filter) {
565         mClasses.add(filter);
566     }
567 
568     @Override
addAllIncludeFilters(Set<String> filters)569     public void addAllIncludeFilters(Set<String> filters) {
570         mClasses.addAll(filters);
571 
572     }
573 
574     @Override
addExcludeFilter(String filter)575     public void addExcludeFilter(String filter) {
576         throw new UnsupportedOperationException("Exclude filter is not supported.");
577     }
578 
579     @Override
addAllExcludeFilters(Set<String> filters)580     public void addAllExcludeFilters(Set<String> filters) {
581         throw new UnsupportedOperationException("Exclude filters is not supported.");
582     }
583 
584     /** {@inheritDoc} */
585     @Override
clearIncludeFilters()586     public void clearIncludeFilters() {
587         mClasses.clear();
588     }
589 
590     /** {@inheritDoc} */
591     @Override
getIncludeFilters()592     public Set<String> getIncludeFilters() {
593         return mClasses;
594     }
595 
596     /** {@inheritDoc} */
597     @Override
getExcludeFilters()598     public Set<String> getExcludeFilters() {
599         throw new UnsupportedOperationException("Exclude filters is not supported.");
600     }
601 
602     /** {@inheritDoc} */
603     @Override
clearExcludeFilters()604     public void clearExcludeFilters() {
605         throw new UnsupportedOperationException("Exclude filters is not supported.");
606     }
607 }
608