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