View Javadoc
1   /*
2    * Copyright (C) 2009 Jayway AB
3    * Copyright (C) 2007-2008 JVending Masa
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package com.simpligility.maven.plugins.android.standalonemojos;
18  
19  import java.io.File;
20  import java.io.IOException;
21  
22  import org.apache.commons.lang3.StringUtils;
23  import org.apache.maven.plugin.MojoExecutionException;
24  import org.apache.maven.plugin.MojoFailureException;
25  import org.apache.maven.plugins.annotations.Mojo;
26  import org.apache.maven.plugins.annotations.Parameter;
27  
28  import com.android.ddmlib.AdbCommandRejectedException;
29  import com.android.ddmlib.IDevice;
30  import com.android.ddmlib.ShellCommandUnresponsiveException;
31  import com.android.ddmlib.TimeoutException;
32  import com.android.ddmlib.testrunner.UIAutomatorRemoteAndroidTestRunner;
33  import com.simpligility.maven.plugins.android.AbstractAndroidMojo;
34  import com.simpligility.maven.plugins.android.AndroidTestRunListener;
35  import com.simpligility.maven.plugins.android.DeviceCallback;
36  import com.simpligility.maven.plugins.android.ScreenshotServiceWrapper;
37  import com.simpligility.maven.plugins.android.common.DeviceHelper;
38  import com.simpligility.maven.plugins.android.config.ConfigHandler;
39  import com.simpligility.maven.plugins.android.config.ConfigPojo;
40  import com.simpligility.maven.plugins.android.config.PullParameter;
41  import com.simpligility.maven.plugins.android.configuration.UIAutomator;
42  
43  /**
44   * Can execute tests using ui uiautomator.<br>
45   * Implements parsing parameters from pom or command line arguments and sets useful defaults as well. This goal is meant
46   * to execute a special <i>java project</i> dedicated to UI testing via UIAutomator. It will build the jar of the
47   * project, dex it and send it to dalvik cache of a rooted device or to an emulator. If you use a rooted device, refer
48   * to <a href="http://stackoverflow.com/a/13805869/693752">this thread on stack over flow</a>. <br>
49   * <br>
50   * The tests are executed via ui automator. A surefire compatible test report can be generated and its location will be
51   * logged during build. <br>
52   * <br>
53   * To use this goal, you will need to place the uiautomator.jar file (part of the Android SDK >= 16) on a nexus
54   * repository. <br>
55   * <br>
56   * A typical usage of this goal can be found at <a
57   * href="https://github.com/stephanenicolas/Quality-Tools-for-Android">Quality tools for Android project</a>.
58   * 
59   * @see <a href="http://developer.android.com/tools/testing/testing_ui.html">Android UI testing doc</a>
60   * @see <a href="http://developer.android.com/tools/help/uiautomator/index.html">UI Automator manual page</a>
61   * @author Stéphane Nicolas - snicolas@octo.com
62   */
63  @SuppressWarnings( "unused" )
64  @Mojo( name = "uiautomator", requiresProject = false )
65  public class UIAutomatorMojo extends AbstractAndroidMojo
66  {
67      /**
68       * -Dmaven.test.skip is commonly used with Maven to skip tests. We honor it.
69       */
70      @Parameter( property = "maven.test.skip", defaultValue = "false", readonly = true )
71      private boolean mavenTestSkip;
72  
73      /**
74       * -DskipTests is commonly used with Maven to skip tests. We honor it too.
75       */
76      @Parameter( property = "skipTests", defaultValue = "false", readonly = true )
77      private boolean mavenSkipTests;
78  
79      /**
80       * -Dmaven.test.failure.ignore is commonly used with Maven to prevent failure of build when (some) tests fail. We
81       * honor it too. Builds will still fail if tests can't complete or throw an exception.
82       */
83      @Parameter( property = "maven.test.failure.ignore", defaultValue = "false", readonly = true )
84      private boolean mavenTestFailureIgnore;
85  
86      /**
87       * -Dmaven.test.failure.ignore is commonly used with Maven to prevent failure of build when (some) tests fail. We
88       * honor it too. Builds will still fail if tests can't complete or throw an exception.
89       */
90      @Parameter( property = "testFailureIgnore", defaultValue = "false", readonly = true )
91      private boolean mavenIgnoreTestFailure;
92  
93      /**
94       * The configuration for the ui automator goal. As soon as a lint goal is invoked the command will be executed
95       * unless the skip parameter is set. A minimal configuration that will run lint and produce a XML report in
96       * ${project.build.directory}/lint/lint-results.xml is
97       * 
98       * <pre>
99       * &lt;uiautomator&gt;
100      *   &lt;skip&gt;false&lt;/skip&gt;
101      * &lt;/uiautomator&gt;
102      * </pre>
103      * 
104      * Full configuration can use these parameters.
105      * 
106      * <pre>
107      * &lt;uiautomator&gt;
108      *   &lt;skip&gt;false&lt;/skip&gt;
109      *   &lt;testClassOrMethods&gt;
110      *     &lt;testClassOrMethod&gt;com.foo.SampleTest&lt;/testClassOrMethod&gt;
111      *     &lt;testClassOrMethod&gt;com.bar.CalculatorTest#testCalculatorApp&lt;/testClassOrMethod&gt;
112      *   &lt;/testClassOrMethods&gt;
113      *   &lt;createReport&gt;true&lt;/createReport&gt;
114      *   &lt;takeScreenshotOnFailure&gt;true&lt;/takeScreenshotOnFailure&gt;
115      *   &lt;screenshotsPathOnDevice&gt;/sdcard/uiautomator-screenshots/&lt;/screenshotsPathOnDevice&gt;
116      *   &lt;propertiesKeyPrefix&gt;UIA&lt;/propertiesKeyPrefix&gt;
117      * &lt;/uiautomator&gt;
118      * </pre>
119      * 
120      * 
121      * Alternatively to the plugin configuration values can also be configured as properties on the command line as
122      * android.lint.* or in pom or settings file as properties like lint*.
123      */
124     @Parameter
125     @ConfigPojo
126     private UIAutomator uiautomator;
127 
128     /**
129      * Enables or disables uiautomator test goal. If <code>true</code> it will be skipped; if <code>false</code>, it
130      * will be run.
131      */
132     @Parameter( property = "android.uiautomator.skip" )
133     private Boolean uiautomatorSkip;
134 
135     @PullParameter( defaultValue = "true" )
136     private Boolean parsedSkip;
137 
138     /**
139      * Jar file that will be run during ui uiautomator tests.
140      */
141     @Parameter( property = "android.uiautomator.jarFile" )
142     private String uiautomatorJarFile;
143 
144     @PullParameter( defaultValueGetterMethod = "getJarFile" )
145     private String parsedJarFile;
146 
147     /**
148      * Test class or methods to execute during uiautomator tests. Each class or method must be fully qualified with the
149      * package name, in one of these formats:
150      * <ul>
151      * <li>package_name.class_name
152      * <li>package_name.class_name#method_name
153      * </ul>
154      */
155     @Parameter( property = "android.uiautomator.testClassOrMethod" )
156     private String[] uiautomatorTestClassOrMethods;
157 
158     @PullParameter( required = false, defaultValueGetterMethod = "getTestClassOrMethods" )
159     private String[] parsedTestClassOrMethods;
160 
161     /**
162      * Decides whether to run the test to completion on the device even if its parent process is terminated (for
163      * example, if the device is disconnected).
164      */
165     @Parameter( property = "android.uiautomator.noHup" )
166     private Boolean uiautomatorNoHup;
167 
168     @PullParameter( defaultValue = "false" )
169     private Boolean parsedNoHup;
170 
171     /**
172      * Decides whether to wait for debugger to connect before starting.
173      */
174     @Parameter( property = "android.uiautomator.debug" )
175     private Boolean uiautomatorDebug = false;
176 
177     @PullParameter( defaultValue = "false" )
178     private Boolean parsedDebug;
179 
180     /**
181      * Decides whether to use a dump file or not.
182      */
183     @Parameter( property = "android.uiautomator.useDump" )
184     private Boolean uiautomatorUseDump;
185 
186     @PullParameter( defaultValue = "false" )
187     private Boolean parsedUseDump;
188 
189     /**
190      * Generate an XML file with a dump of the current UI hierarchy. If a filepath is not specified, by default, the
191      * generated dump file is stored on the device in this location /storage/sdcard0/window_dump.xml.
192      */
193     @Parameter( property = "android.uiautomator.dumpFilePath" )
194     private String uiautomatorDumpFilePath;
195 
196     @PullParameter( required = false, defaultValue = "/storage/sdcard0/window_dump.xml" )
197     private String parsedDumpFilePath;
198 
199     /**
200      * Create a junit xml format compatible output file containing the test results for each device the instrumentation
201      * tests run on. <br>
202      * <br>
203      * The files are stored in target/surefire-reports and named TEST-deviceid.xml. The deviceid for an emulator is
204      * deviceSerialNumber_avdName_manufacturer_model. The serial number is commonly emulator-5554 for the first emulator
205      * started with numbers increasing. avdName is as defined in the SDK tool. The manufacturer is typically "unknown"
206      * and the model is typically "sdk".<br>
207      * The deviceid for an actual devices is deviceSerialNumber_manufacturer_model. <br>
208      * <br>
209      * The file contains system properties from the system running the Android Maven Plugin (JVM) and device properties
210      * from the device/emulator the tests are running on. <br>
211      * <br>
212      * The file contains a single TestSuite for all tests and a TestCase for each test method. Errors and failures are
213      * logged in the file and the system log with full stack traces and other details available.
214      */
215     @Parameter( property = "android.uiautomator.createReport" )
216     private Boolean uiautomatorCreateReport;
217 
218     @PullParameter( defaultValue = "false" )
219     private Boolean parsedCreateReport;
220 
221     /**
222      * Adds a suffix to the report name. For example if parameter reportSuffix is "-mySpecialReport",
223      * the name of the report will be TEST-deviceid-mySpecialReport.xml
224      *
225      * Defaults to null. Hence, in the default case, the name of the report will be TEST-deviceid.xml.
226      */
227     @Parameter( property = "android.uiautomator.reportSuffix" )
228     private String uiautomatorReportSuffix;
229 
230     @PullParameter( required = false, defaultValueGetterMethod = "getReportSuffix" )
231     private String parsedReportSuffix;
232 
233     /**
234      * Decides whether or not to take screenshots when tests execution results in failure or error. Screenshots use the
235      * utiliy screencap that is usually available within emulator/devices with SDK >= 16.
236      */
237     @Parameter( property = "android.uiautomator.takeScreenshotOnFailure" )
238     private Boolean uiautomatorTakeScreenshotOnFailure;
239 
240     @PullParameter( defaultValue = "false" )
241     private Boolean parsedTakeScreenshotOnFailure;
242 
243     /**
244      * Location of the screenshots on device. This value is only taken into account if takeScreenshotOnFailure = true.
245      * If a filepath is not specified, by default, the screenshots will be located at /sdcard/uiautomator-screenshots/.
246      */
247     @Parameter( property = "android.uiautomator.screenshotsPathOnDevice" )
248     private String uiautomatorScreenshotsPathOnDevice;
249 
250     @PullParameter( required = false, defaultValue = "/sdcard/uiautomator-screenshots/" )
251     private String parsedScreenshotsPathOnDevice;
252     
253     /**
254      * <p>Specifies a prefix for custom user properties that will be sent 
255      * through to UIAutomator with the <code>"-e key value"</code> parameter.</p>
256      * 
257      * <p>If any user property is needed in a test case, this is the way to send it through.
258      * User credentials for example.</p>
259      * 
260      * <p>If no prefix value is specified no user property will be sent.</p>
261      * 
262      * <p>Usage example:</p>
263      * <p><code>&lt;propertiesKeyPrefix&gt;UIA&lt;/propertiesKeyPrefix&gt;</code></p>
264      * <p>And run it with:</p>
265      * <p><code>&gt; mvn &lt;goal&gt; "-DUIAkey=value"</code></p>
266      * <p>would become <code>"-e key value"</code> as it would be runned from adb</p>
267      */
268     @Parameter( property = "android.uiautomator.propertiesKeyPrefix" )
269     private String uiautomatorPropertiesKeyPrefix;
270 
271     @PullParameter( required = false, defaultValueGetterMethod = "getPropertiesKeyPrefix" )
272     private String parsedPropertiesKeyPrefix;
273 
274     @Override
275     public void execute() throws MojoExecutionException, MojoFailureException
276     {
277         ConfigHandler configHandler = new ConfigHandler( this, this.session, this.execution );
278         configHandler.parseConfiguration();
279 
280         if ( isEnableIntegrationTest() )
281         {
282             playTests();
283         }
284     }
285 
286     /**
287      * Whether or not tests are enabled.
288      * 
289      * @return a boolean indicating whether or not tests are enabled.
290      */
291     protected boolean isEnableIntegrationTest()
292     {
293         return !parsedSkip && !mavenTestSkip && !mavenSkipTests;
294     }
295 
296     /**
297      * Whether or not test failures should be ignored.
298      * 
299      * @return a boolean indicating whether or not test failures should be ignored.
300      */
301     protected boolean isIgnoreTestFailures()
302     {
303         return mavenIgnoreTestFailure || mavenTestFailureIgnore;
304     }
305 
306     /**
307      * Actually plays tests.
308      * 
309      * @throws MojoExecutionException
310      *             if at least a test threw an exception and isIgnoreTestFailures is false..
311      * @throws MojoFailureException
312      *             if at least a test failed and isIgnoreTestFailures is false.
313      */
314     protected void playTests() throws MojoExecutionException, MojoFailureException
315     {
316 
317         getLog().debug( "Parsed values for Android UI UIAutomator invocation: " );
318         getLog().debug( "jarFile:" + parsedJarFile );
319         String testClassOrMethodString = buildSpaceSeparatedString( parsedTestClassOrMethods );
320         getLog().debug( "testClassOrMethod:" + testClassOrMethodString );
321         getLog().debug( "createReport:" + parsedCreateReport );
322 
323         DeviceCallback instrumentationTestExecutor = new DeviceCallback()
324         {
325             @Override
326             public void doWithDevice( final IDevice device ) throws MojoExecutionException, MojoFailureException
327             {
328                 String deviceLogLinePrefix = DeviceHelper.getDeviceLogLinePrefix( device );
329 
330                 UIAutomatorRemoteAndroidTestRunner automatorRemoteAndroidTestRunner //
331                 = new UIAutomatorRemoteAndroidTestRunner( parsedJarFile, device );
332 
333                 automatorRemoteAndroidTestRunner.setRunName( "ui uiautomator tests" );
334                 automatorRemoteAndroidTestRunner.setDebug( uiautomatorDebug );
335                 automatorRemoteAndroidTestRunner.setTestClassOrMethods( parsedTestClassOrMethods );
336                 automatorRemoteAndroidTestRunner.setNoHup( parsedNoHup );
337                 automatorRemoteAndroidTestRunner.setUserProperties( session.getUserProperties(), 
338                         parsedPropertiesKeyPrefix );
339                 
340                 if ( parsedUseDump )
341                 {
342                     automatorRemoteAndroidTestRunner.setDumpFilePath( parsedDumpFilePath );
343                 }
344 
345                 getLog().info( deviceLogLinePrefix + "Running ui uiautomator tests in" + parsedJarFile );
346                 try
347                 {
348                     AndroidTestRunListener testRunListener = new AndroidTestRunListener( device, getLog(),
349                             parsedCreateReport, parsedTakeScreenshotOnFailure, parsedScreenshotsPathOnDevice,
350                             parsedReportSuffix, targetDirectory );
351                     automatorRemoteAndroidTestRunner.run( testRunListener );
352                     if ( testRunListener.hasFailuresOrErrors() && !isIgnoreTestFailures() )
353                     {
354                         throw new MojoFailureException( deviceLogLinePrefix + "Tests failed on device." );
355                     }
356                     if ( testRunListener.testRunFailed() )
357                     {
358                         throw new MojoFailureException( deviceLogLinePrefix + "Test run failed to complete: "
359                                 + testRunListener.getTestRunFailureCause() );
360                     }
361                     if ( testRunListener.threwException() && !isIgnoreTestFailures() )
362                     {
363                         throw new MojoFailureException( deviceLogLinePrefix + testRunListener.getExceptionMessages() );
364                     }
365                 }
366                 catch ( TimeoutException e )
367                 {
368                     throw new MojoExecutionException( deviceLogLinePrefix + "timeout", e );
369                 }
370                 catch ( AdbCommandRejectedException e )
371                 {
372                     throw new MojoExecutionException( deviceLogLinePrefix + "adb command rejected", e );
373                 }
374                 catch ( ShellCommandUnresponsiveException e )
375                 {
376                     throw new MojoExecutionException( deviceLogLinePrefix + "shell command " + "unresponsive", e );
377                 }
378                 catch ( IOException e )
379                 {
380                     throw new MojoExecutionException( deviceLogLinePrefix + "IO problem", e );
381                 }
382             }
383         };
384 
385         instrumentationTestExecutor = new ScreenshotServiceWrapper( instrumentationTestExecutor, project, getLog() );
386 
387         doWithDevices( instrumentationTestExecutor );
388     }
389 
390     private String getJarFile()
391     {
392         if ( parsedJarFile == null )
393         {
394             File jarFilePath = new File( targetDirectory + File.separator
395                     + finalName + ".jar" );
396             return jarFilePath.getName();
397         }
398         return parsedJarFile;
399     }
400 
401     private String[] getTestClassOrMethods()
402     {
403         // null if not overriden by configuration
404         return parsedTestClassOrMethods;
405     }
406 
407     private String getReportSuffix()
408     {
409         return parsedReportSuffix;
410     }
411     
412     private String getPropertiesKeyPrefix()
413     {
414         return parsedPropertiesKeyPrefix;
415     }
416 
417     /**
418      * Helper method to build a comma separated string from a list. Blank strings are filtered out
419      * 
420      * @param lines
421      *            A list of strings
422      * @return Comma separated String from given list
423      */
424     protected static String buildSpaceSeparatedString( String[] lines )
425     {
426         if ( lines == null || lines.length == 0 )
427         {
428             return null;
429         }
430 
431         return StringUtils.join( lines, " " );
432     }
433 }