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.AdbCommandRejectedException;
20 import com.android.ddmlib.IDevice;
21 import com.android.ddmlib.ShellCommandUnresponsiveException;
22 import com.android.ddmlib.TimeoutException;
23 import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
24 import com.android.ddmlib.testrunner.ITestRunListener;
25 import com.android.ddmlib.testrunner.InstrumentationResultParser;
26 import com.android.tradefed.log.LogUtil.CLog;
27 import com.android.tradefed.util.ArrayUtil;
28 
29 import java.io.IOException;
30 import java.util.Arrays;
31 import java.util.Collection;
32 import java.util.LinkedHashMap;
33 import java.util.Map;
34 import java.util.Map.Entry;
35 import java.util.concurrent.TimeUnit;
36 
37 /**
38  * Runs UI Automator test on device and reports results.
39  *
40  * UI Automator test is a dedicated test runner for running UI automation tests that
41  * utilizes UI Automator framework. The test runner on device emulates instrumentation
42  * test output format so that existing parsing code in ddmlib and TF can be reused.
43  *
44  * Essentially, this is a wrapper around this command:
45  * adb shell uiautomator runtest (jar files) -e class (test classes) ...
46  *
47  */
48 public class UiAutomatorRunner implements IRemoteAndroidTestRunner {
49 
50     private static final String CLASS_ARG_NAME = "class";
51     private static final String DEBUG_ARG_NAME = "debug";
52     private static final String RUNNER_ARG_NAME = "runner";
53     private static final char METHOD_SEPARATOR = '#';
54     private static final char CLASS_SEPARATOR = ',';
55     private static final String DEFAULT_RUNNER_NAME =
56             "com.android.uiautomator.testrunner.UiAutomatorTestRunner";
57     private static final String UIAUTOMATOR_RUNNER_PATH = "/system/bin/uiautomator";
58 
59     private Map<String, String> mArgsMap;
60     private String[] mJarPaths;
61     private String mPackageName;
62     // default to no timeout
63     private long mMaxTimeout = 0l;
64     private long mMaxTimeToOutputResponse = 0;
65     private IDevice mRemoteDevice;
66     private String mRunName;
67     private InstrumentationResultParser mParser;
68     private String mRunnerPath = UIAUTOMATOR_RUNNER_PATH;
69     private String mRunnerName = DEFAULT_RUNNER_NAME;
70     private boolean mIgnoreSighup = false;
71 
72     /**
73      * Create a UiAutomatorRunner for running UI automation tests
74      *
75      * @param remoteDevice the remote device to interact with: run test, collect results etc
76      * @param jarPaths the paths to jar files where UI Automator test cases are; the paths must be
77      *                absolute or relative to /data/local/tmp/ on device
78      * @param runnerPath alternative uiautomator runner to use, may be <code>null</code> and default
79      *                   will be used in this case
80      */
UiAutomatorRunner(IDevice remoteDevice, String[] jarPaths, String runnerPath)81     public UiAutomatorRunner(IDevice remoteDevice, String[] jarPaths, String runnerPath) {
82         mRemoteDevice = remoteDevice;
83         mJarPaths = jarPaths;
84         mArgsMap = new LinkedHashMap<String, String>(); // ensure the order that the args are added
85         if (runnerPath != null) {
86             mRunnerPath = runnerPath;
87         }
88     }
89 
90     /**
91      * Returns the package name of last Java class added
92      */
93     @Override
getPackageName()94     public String getPackageName() {
95         return mPackageName;
96     }
97 
98     /**
99      * Returns default UiAutomatorTestRunner class name
100      */
101     @Override
getRunnerName()102     public String getRunnerName() {
103         return mRunnerName;
104     }
105 
getRunnerPath()106     protected String getRunnerPath() {
107         return mRunnerPath;
108     }
109 
110     /**
111      * Sets the option in the uiautomator to ignore SIGHUP.
112      * @param value ignore the signal if set to true
113      */
setIgnoreSighup(boolean value)114     public void setIgnoreSighup(boolean value) {
115         mIgnoreSighup = value;
116     }
117 
getRunCommand()118     protected String getRunCommand() {
119         String jarArg = ArrayUtil.join(" ", (Object[])mJarPaths);
120         String command = String.format("%s runtest %s %s",
121                 getRunnerPath(), jarArg, getArgsCommand());
122         if (mIgnoreSighup) {
123             return command + " --nohup";
124         } else {
125             return command;
126         }
127     }
128 
129     /**
130      * Returns the full instrumentation command line syntax for the provided instrumentation
131      * arguments.
132      * Returns an empty string if no arguments were specified.
133      */
getArgsCommand()134     private String getArgsCommand() {
135         StringBuilder commandBuilder = new StringBuilder();
136         for (Entry<String, String> argPair : mArgsMap.entrySet()) {
137             final String argCmd = String.format(" -e %1$s %2$s", argPair.getKey(),
138                     argPair.getValue());
139             commandBuilder.append(argCmd);
140         }
141         return commandBuilder.toString();
142     }
143 
144     /**
145      * {@inheritDoc}
146      */
147     @Override
setClassName(String className)148     public void setClassName(String className) {
149         int pos = className.lastIndexOf('.');
150         // get package name segment
151         if (pos == -1) {
152             mPackageName = "(default)";
153         } else {
154             mPackageName = className.substring(0, pos);
155         }
156         addInstrumentationArg(CLASS_ARG_NAME, className);
157     }
158 
159     /**
160      * {@inheritDoc}
161      */
162     @Override
setClassNames(String[] classNames)163     public void setClassNames(String[] classNames) {
164         StringBuilder classArgBuilder = new StringBuilder();
165 
166         for (int i = 0; i < classNames.length; i++) {
167             if (i != 0) {
168                 classArgBuilder.append(CLASS_SEPARATOR);
169             }
170             classArgBuilder.append(classNames[i]);
171         }
172         setClassName(classArgBuilder.toString());
173     }
174 
175     /**
176      * {@inheritDoc}
177      */
178     @Override
setMethodName(String className, String testName)179     public void setMethodName(String className, String testName) {
180         setClassName(className + METHOD_SEPARATOR + testName);
181     }
182 
183     /**
184      * {@inheritDoc}
185      */
186     @Override
setTestPackageName(String packageName)187     public void setTestPackageName(String packageName) {
188         throw new UnsupportedOperationException("specifying package name is not supported");
189     }
190 
191     /**
192      * {@inheritDoc}
193      */
194     @Override
setTestSize(TestSize size)195     public void setTestSize(TestSize size) {
196         throw new UnsupportedOperationException("specifying test size is not supported");
197     }
198 
199     /**
200      * {@inheritDoc}
201      */
202     @Override
addInstrumentationArg(String name, String value)203     public void addInstrumentationArg(String name, String value) {
204         if (name == null) {
205             throw new IllegalArgumentException("name cannot be null");
206         }
207         if (RUNNER_ARG_NAME.equals(name)) {
208             mRunnerName = name;
209         }
210         mArgsMap.put(name, value);
211     }
212 
213     /**
214      * {@inheritDoc}
215      */
216     @Override
removeInstrumentationArg(String name)217     public void removeInstrumentationArg(String name) {
218         if (name == null) {
219             throw new IllegalArgumentException("name cannot be null");
220         }
221         mArgsMap.remove(name);
222     }
223 
224     /**
225      * {@inheritDoc}
226      */
227     @Override
addBooleanArg(String name, boolean value)228     public void addBooleanArg(String name, boolean value) {
229         addInstrumentationArg(name, Boolean.toString(value));
230     }
231 
232     /**
233      * {@inheritDoc}
234      */
235     @Override
setLogOnly(boolean logOnly)236     public void setLogOnly(boolean logOnly) {
237         //TODO: we need to support log only for Eclipse and re-run support
238         throw new UnsupportedOperationException("log only mode is not supported");
239     }
240 
241     /**
242      * {@inheritDoc}
243      */
244     @Override
setDebug(boolean debug)245     public void setDebug(boolean debug) {
246         addBooleanArg(DEBUG_ARG_NAME, debug);
247     }
248 
249     /**
250      * {@inheritDoc}
251      */
252     @Override
setCoverage(boolean coverage)253     public void setCoverage(boolean coverage) {
254         // TODO potentially it's possible to run with coverage, need more investigation
255         throw new UnsupportedOperationException("coverage mode is not supported");
256     }
257 
258     @Override
getCoverageOutputType()259     public CoverageOutput getCoverageOutputType() {
260         throw new UnsupportedOperationException("coverage mode is not supported");
261     }
262 
263     @Override
setCoverageReportLocation(String arg0)264     public void setCoverageReportLocation(String arg0) {
265         throw new UnsupportedOperationException("coverage mode is not supported");
266     }
267 
268     @Override
setTestCollection(boolean b)269     public void setTestCollection(boolean b) {
270         throw new UnsupportedOperationException("Test Collection mode is not supported");
271     }
272 
273     /**
274      * {@inheritDoc}
275      * @deprecated use {@link #setMaxTimeToOutputResponse(long, TimeUnit)} instead.
276      */
277     @Deprecated
278     @Override
setMaxtimeToOutputResponse(int maxTimeToOutputResponse)279     public void setMaxtimeToOutputResponse(int maxTimeToOutputResponse) {
280         setMaxTimeToOutputResponse(maxTimeToOutputResponse, TimeUnit.MILLISECONDS);
281     }
282 
283     /**
284      * {@inheritDoc}
285      */
286     @Override
setMaxTimeToOutputResponse(long timeout, TimeUnit unit)287     public void setMaxTimeToOutputResponse(long timeout, TimeUnit unit) {
288         mMaxTimeToOutputResponse = unit.toMillis(timeout);
289     }
290 
291     /**
292      * {@inheritDoc}
293      */
294     @Override
setRunName(String runName)295     public void setRunName(String runName) {
296         mRunName = runName;
297     }
298 
299     /**
300      * {@inheritDoc}
301      */
302     @Override
run(ITestRunListener... listeners)303     public void run(ITestRunListener... listeners) throws TimeoutException,
304             AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
305         run(Arrays.asList(listeners));
306     }
307 
308     /**
309      * {@inheritDoc}
310      */
311     @Override
run(Collection<ITestRunListener> listeners)312     public void run(Collection<ITestRunListener> listeners) throws TimeoutException,
313             AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
314         String cmdLine = getRunCommand();
315         CLog.i("Running %s on %s", cmdLine, mRemoteDevice.getSerialNumber());
316         String runName = mRunName == null ? mPackageName : mRunName;
317         mParser = new InstrumentationResultParser(runName, listeners);
318 
319         try {
320             mRemoteDevice.executeShellCommand(
321                     cmdLine, mParser, mMaxTimeout, mMaxTimeToOutputResponse, TimeUnit.MILLISECONDS);
322         } catch (IOException e) {
323             CLog.w(String.format("IOException %1$s when running tests %2$s on %3$s",
324                     e.toString(), getPackageName(), mRemoteDevice.getSerialNumber()));
325             // rely on parser to communicate results to listeners
326             mParser.handleTestRunFailed(e.toString());
327             throw e;
328         } catch (ShellCommandUnresponsiveException e) {
329             CLog.w("ShellCommandUnresponsiveException %1$s when running tests %2$s on %3$s",
330                     e.toString(), getPackageName(), mRemoteDevice.getSerialNumber());
331             mParser.handleTestRunFailed(String.format(
332                     "Failed to receive adb shell test output within %1$d ms. " +
333                     "Test may have timed out, or adb connection to device became unresponsive",
334                     mMaxTimeToOutputResponse));
335             throw e;
336         } catch (TimeoutException e) {
337             CLog.w("TimeoutException when running tests %1$s on %2$s", getPackageName(),
338                     mRemoteDevice.getSerialNumber());
339             mParser.handleTestRunFailed(e.toString());
340             throw e;
341         } catch (AdbCommandRejectedException e) {
342             CLog.w("AdbCommandRejectedException %1$s when running tests %2$s on %3$s",
343                     e.toString(), getPackageName(), mRemoteDevice.getSerialNumber());
344             mParser.handleTestRunFailed(e.toString());
345             throw e;
346         }
347     }
348 
349     /**
350      * {@inheritDoc}
351      */
352     @Override
cancel()353     public void cancel() {
354         if (mParser != null) {
355             mParser.cancel();
356         }
357     }
358 
359     @Override
setMaxTimeout(long maxTimeout, TimeUnit unit)360     public void setMaxTimeout(long maxTimeout, TimeUnit unit) {
361         mMaxTimeout = unit.toMillis(maxTimeout);
362     }
363 
364     @Override
setAdditionalTestOutputLocation(String additionalTestDataPath)365     public void setAdditionalTestOutputLocation(String additionalTestDataPath) {
366         addInstrumentationArg("additionalTestOutputDir", additionalTestDataPath);
367     }
368 }
369