All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.jayway.maven.plugins.android.standalonemojos.MonkeyMojo Maven / Gradle / Ivy

There is a newer version: 4.0.0-rc.2
Show newest version
/*
 * Copyright (C) 2009 Jayway AB
 * Copyright (C) 2007-2008 JVending Masa
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.jayway.maven.plugins.android.standalonemojos;

import static com.android.ddmlib.testrunner.ITestRunListener.TestFailure.ERROR;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Date;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;

import com.android.ddmlib.AdbCommandRejectedException;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.IShellOutputReceiver;
import com.android.ddmlib.ShellCommandUnresponsiveException;
import com.android.ddmlib.TimeoutException;
import com.android.ddmlib.testrunner.ITestRunListener;
import com.android.ddmlib.testrunner.MonkeyTestRunner;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.jayway.maven.plugins.android.AbstractAndroidMojo;
import com.jayway.maven.plugins.android.DeviceCallback;
import com.jayway.maven.plugins.android.common.DeviceHelper;
import com.jayway.maven.plugins.android.config.ConfigHandler;
import com.jayway.maven.plugins.android.config.ConfigPojo;
import com.jayway.maven.plugins.android.config.PullParameter;
import com.jayway.maven.plugins.android.configuration.Monkey;

/**
 * Can execute tests using UI/Application Exerciser Monkey.
* Implements parsing parameters from pom or command line arguments and sets useful defaults as well. This goal will * invoke Android Monkey exerciser. If the application crashes during the exercise, this goal can fail the build.
* A typical usage of this goal can be found at Quality tools for Android project. * * @see Monkey docs by Google * @see Stack Over Flow thread for parsing monkey output. * @author Stéphane Nicolas * @goal monkey * @requiresProject true */ @SuppressWarnings( "unused" ) public class MonkeyMojo extends AbstractAndroidMojo { /** * -Dmaven.test.skip is commonly used with Maven to skip tests. We honor it. * * @parameter expression="${maven.test.skip}" default-value=false * @readonly */ private boolean mavenTestSkip; /** * -DskipTests is commonly used with Maven to skip tests. We honor it too. * * @parameter expression="${skipTests}" default-value=false * @readonly */ private boolean mavenSkipTests; /** * -Dmaven.test.failure.ignore is commonly used with Maven to prevent failure of build when (some) tests fail. We * honor it too. * * @parameter expression="${maven.test.failure.ignore}" default-value=false * @readonly */ private boolean mavenTestFailureIgnore; /** * -Dmaven.test.failure.ignore is commonly used with Maven to prevent failure of build when (some) tests fail. We * honor it too. * * @parameter expression="${testFailureIgnore}" default-value=false * @readonly */ private boolean mavenIgnoreTestFailure; /** * The configuration for the ui automator goal. As soon as a lint goal is invoked the command will be executed * unless the skip parameter is set. A minimal configuration that will run lint and produce a XML report in * ${project.build.directory}/lint/lint-results.xml is * *
     * <monkey>
     *   <skip>false</skip>
     * </monkey>
     * 
* * Full configuration can use these parameters. * *
     *  <monkey>
     *    <skip>false</skip>
     *    <eventCount>5000</eventCount>
     *    <seed>123456</seed>
     *    <throttle>10</throttle>
     *    <percentTouch>10</percentTouch>
     *    <percentMotion>10</percentMotion>
     *    <percentTrackball>10</percentTrackball>
     *    <percentNav>10</percentNav>
     *    <percentMajorNav>10</percentMajorNav>
     *    <percentSyskeys>10</percentSyskeys>
     *    <percentAppswitch>10</percentAppswitch>
     *    <percentAnyevent>10</percentAnyevent>
     *    <packages>
     *        <package>com.foo</package>
     *        <package>com.bar</package>
     *    </packages>
     *    <categories>
     *        <category>foo</category>
     *        <category>bar</category>
     *    </categories>
     *    <debugNoEvents>true</debugNoEvents>
     *    <hprof>true</hprof>
     *    <ignoreCrashes>true</ignoreCrashes>
     *    <ignoreTimeouts>true</ignoreTimeouts>
     *    <ignoreSecurityExceptions>true</ignoreSecurityExceptions>
     *    <killProcessAfterError>true</killProcessAfterError>
     *    <monitorNativeCrashes>true</monitorNativeCrashes>
     *    <createReport>true</createReport>
     *  </monkey>
     * 
* * Alternatively to the plugin configuration values can also be configured as properties on the command line as * android.lint.* or in pom or settings file as properties like lint*. * * @parameter */ @ConfigPojo private Monkey monkey; /** * Enables or disables monkey test goal. If true it will be skipped; if false, it will be * run. Defaults to true. * * @parameter expression="${android.monkey.skip}" */ private Boolean monkeySkip; @PullParameter( defaultValue = "true" ) private Boolean parsedSkip; /** * Number of generated events. Defaults to 1000. * * @parameter expression="${android.monkey.eventCount}" */ private Integer monkeyEventCount; @PullParameter( required = true, defaultValue = "1000" ) private Integer parsedEventCount; /** * Seed value for pseudo-random number generator. If you re-run the Monkey with the same seed value, it will * generate the same sequence of events. * * Defaults to null. * * @parameter expression="${android.monkey.seed}" */ private Long monkeySeed; @PullParameter( required = false, defaultValueGetterMethod = "getSeed" ) private Long parsedSeed; /** * Inserts a fixed delay between events. You can use this option to slow down the Monkey. If not specified, there is * no delay and the events are generated as rapidly as possible. * * Defaults to null. * * @parameter expression="${android.monkey.throttle}" */ private Long monkeyThrottle; @PullParameter( required = false, defaultValueGetterMethod = "getThrottle" ) private Long parsedThrottle; /** * Adjust percentage of touch events. (Touch events are a down-up event in a single place on the screen.) * * Defaults to null. * * @parameter expression="${android.monkey.percentTouch}" */ private Integer monkeyPercentTouch; @PullParameter( required = false, defaultValueGetterMethod = "getPercentTouch" ) private Integer parsedPercentTouch; /** * Adjust percentage of motion events. (Motion events consist of a down event somewhere on the screen, a series of * pseudo-random movements, and an up event.) * * Defaults to null. * * @parameter expression="${android.monkey.percentMotion}" */ private Integer monkeyPercentMotion; @PullParameter( required = false, defaultValueGetterMethod = "getPercentMotion" ) private Integer parsedPercentMotion; /** * Adjust percentage of trackball events. (Trackball events consist of one or more random movements, sometimes * followed by a click.) * * Defaults to null. * * @parameter expression="${android.monkey.percentTrackball}" */ private Integer monkeyPercentTrackball; @PullParameter( required = false, defaultValueGetterMethod = "getPercentTrackball" ) private Integer parsedPercentTrackball; /** * Adjust percentage of "basic" navigation events. (Navigation events consist of up/down/left/right, as input from a * directional input device.) * * Defaults to null. * * @parameter expression="${android.monkey.percentNav}" */ private Integer monkeyPercentNav; @PullParameter( required = false, defaultValueGetterMethod = "getPercentNav" ) private Integer parsedPercentNav; /** * Adjust percentage of "major" navigation events. (These are navigation events that will typically cause actions * within your UI, such as the center button in a 5-way pad, the back key, or the menu key.) * * Defaults to null. * * @parameter expression="${android.monkey.percentMajorNav}" */ private Integer monkeyPercentMajorNav; @PullParameter( required = false, defaultValueGetterMethod = "getPercentMajorNav" ) private Integer parsedPercentMajorNav; /** * Adjust percentage of "system" key events. (These are keys that are generally reserved for use by the system, such * as Home, Back, Start Call, End Call, or Volume controls.) Defaults to null. * * Defaults to null. * * @parameter expression="${android.monkey.percentSyskeys}" */ private Integer monkeyPercentSyskeys; @PullParameter( required = false, defaultValueGetterMethod = "getPercentSyskeys" ) private Integer parsedPercentSyskeys; /** * Adjust percentage of activity launches. At random intervals, the Monkey will issue a startActivity() call, as a * way of maximizing coverage of all activities within your package. * * Defaults to null. * * @parameter expression="${android.monkey.percentAppswitch}" */ private Integer monkeyPercentAppswitch; @PullParameter( required = false, defaultValueGetterMethod = "getPercentAppswitch" ) private Integer parsedPercentAppswitch; /** * Adjust percentage of other types of events. This is a catch-all for all other types of events such as keypresses, * other less-used buttons on the device, and so forth. * * Defaults to null. * * @parameter expression="${android.monkey.percentAnyevent}" */ private Integer monkeyPercentAnyEvent; @PullParameter( required = false, defaultValueGetterMethod = "getPercentAnyevent" ) private Integer parsedPercentAnyevent; /** * If you specify one or more packages this way, the Monkey will only allow the system to visit activities within * those packages. If your application requires access to activities in other packages (e.g. to select a contact) * you'll need to specify those packages as well. If you don't specify any packages, the Monkey will allow the * system to launch activities in all packages. * * Defaults to Null. * * @parameter expression="${android.monkey.packages}" */ private String[] monkeyPackages; @PullParameter( required = false, defaultValueGetterMethod = "getPackages" ) private String[] parsedPackages; /** * If you specify one or more categories this way, the Monkey will only allow the system to visit activities that * are listed with one of the specified categories. If you don't specify any categories, the Monkey will select * activities listed with the category Intent.CATEGORY_LAUNCHER or Intent.CATEGORY_MONKEY. * * Defaults to null. * * @parameter expression="${android.monkey.categories}" */ private String[] monkeyCategories; @PullParameter( required = false, defaultValueGetterMethod = "getCategories" ) private String[] parsedCategories; /** * When specified, the Monkey will perform the initial launch into a test activity, but will not generate any * further events. For best results, combine with -v, one or more package constraints, and a non-zero throttle to * keep the Monkey running for 30 seconds or more. This provides an environment in which you can monitor package * transitions invoked by your application. * * Defaults to false. * * @parameter expression="${android.monkey.debugNoEvents}" */ private Boolean monkeyDebugNoEvents; @PullParameter( defaultValue = "false" ) private Boolean parsedDebugNoEvents; /** * If set, this option will generate profiling reports immediately before and after the Monkey event sequence. This * will generate large (~5Mb) files in data/misc, so use with care. See Traceview for more information on trace * files. * * Defaults to false. * * @parameter expression="${android.monkey.Hprof}" */ private Boolean monkeyHprof; @PullParameter( defaultValue = "false" ) private Boolean parsedHprof; /** * Normally, the Monkey will stop when the application crashes or experiences any type of unhandled exception. If * you specify this option, the Monkey will continue to send events to the system, until the count is completed. * Settings this option is different to setting testFailureIgnore or maven.test.failure.ignore to true, it will * impact monkey run but not the result of the maven build. * * Defaults to false. * * @parameter expression="${android.monkey.ignoreCrashes}" */ private Boolean monkeyIgnoreCrashes; @PullParameter( defaultValue = "false" ) private Boolean parsedIgnoreCrashes; /** * Normally, the Monkey will stop when the application experiences any type of timeout error such as a * "Application Not Responding" dialog. If you specify this option, the Monkey will continue to send events to the * system, until the count is completed. * * Defaults to false. * * @parameter expression="${android.monkey.IgnoreTimeouts}" */ private Boolean monkeyIgnoreTimeouts; @PullParameter( defaultValue = "false" ) private Boolean parsedIgnoreTimeouts; /** * Normally, the Monkey will stop when the application experiences any type of permissions error, for example if it * attempts to launch an activity that requires certain permissions. If you specify this option, the Monkey will * continue to send events to the system, until the count is completed. * * * Defaults to false. * * @parameter expression="${android.monkey.IgnoreSecurityExceptions}" */ private Boolean monkeyIgnoreSecurityExceptions; @PullParameter( defaultValue = "false" ) private Boolean parsedIgnoreSecurityExceptions; /** * Normally, when the Monkey stops due to an error, the application that failed will be left running. When this * option is set, it will signal the system to stop the process in which the error occurred. Note, under a normal * (successful) completion, the launched process(es) are not stopped, and the device is simply left in the last * state after the final event. * * Defaults to false. * * @parameter expression="${android.monkey.KillProcessAfterError}" */ private Boolean monkeyKillProcessAfterError; @PullParameter( defaultValue = "false" ) private Boolean parsedKillProcessAfterError; /** * Watches for and reports crashes occurring in the Android system native code. If --kill-process-after-error is * set, the system will stop. * * Defaults to false. * * @parameter expression="${android.monkey.MonitorNativeCrashes}" */ private Boolean monkeyMonitorNativeCrashes; @PullParameter( defaultValue = "false" ) private Boolean parsedMonitorNativeCrashes; /** * Create a junit xml format compatible output file containing the test results for each device the instrumentation * tests run on.
*
* The files are stored in target/surefire-reports and named TEST-deviceid.xml. The deviceid for an emulator is * deviceSerialNumber_avdName_manufacturer_model. The serial number is commonly emulator-5554 for the first emulator * started with numbers increasing. avdName is as defined in the SDK tool. The manufacturer is typically "unknown" * and the model is typically "sdk".
* The deviceid for an actual devices is deviceSerialNumber_manufacturer_model.
*
* The file contains system properties from the system running the Android Maven Plugin (JVM) and device properties * from the device/emulator the tests are running on.
*
* The file contains a single TestSuite for all tests and a TestCase for each test method. Errors and failures are * logged in the file and the system log with full stack traces and other details available. * * Defaults to false. * * @optional * @parameter expression="${android.monkey.createReport}" * */ private Boolean monkeyCreateReport; @PullParameter( defaultValue = "false" ) private Boolean parsedCreateReport; @Override public void execute() throws MojoExecutionException, MojoFailureException { ConfigHandler configHandler = new ConfigHandler( this ); configHandler.parseConfiguration(); if ( isEnableIntegrationTest() ) { exerciseApp(); } } /** * Whether or not tests are enabled. * * @return a boolean indicating whether or not tests are enabled. */ protected boolean isEnableIntegrationTest() { return !parsedSkip && !mavenTestSkip && !mavenSkipTests; } /** * Whether or not test failures should be ignored. * * @return a boolean indicating whether or not test failures should be ignored. */ protected boolean isIgnoreTestFailures() { return mavenIgnoreTestFailure || mavenTestFailureIgnore; } /** * Actually plays tests. * * @throws MojoExecutionException * if exercising app threw an exception and isIgnoreTestFailures is false.. * @throws MojoFailureException * if exercising app failed and isIgnoreTestFailures is false. */ protected void exerciseApp() throws MojoExecutionException, MojoFailureException { getLog().debug( "Parsed values for Android Monkey invocation: " ); getLog().debug( "seed:" + parsedSeed ); DeviceCallback instrumentationTestExecutor = new DeviceCallback() { @Override public void doWithDevice( final IDevice device ) throws MojoExecutionException, MojoFailureException { String deviceLogLinePrefix = DeviceHelper.getDeviceLogLinePrefix( device ); MonkeyTestRunner monkeyTestRunner = new MonkeyTestRunner( parsedEventCount, device ); monkeyTestRunner.setRunName( "ui monkey tests" ); if ( parsedSeed != null ) { monkeyTestRunner.setSeed( parsedSeed ); } if ( parsedPercentTouch != null ) { monkeyTestRunner.setPercentTouch( parsedPercentTouch ); } if ( parsedPercentMotion != null ) { monkeyTestRunner.setPercentTouch( parsedPercentMotion ); } if ( parsedPercentTrackball != null ) { monkeyTestRunner.setPercentTrackball( parsedPercentTrackball ); } if ( parsedPercentNav != null ) { monkeyTestRunner.setPercentNav( parsedPercentNav ); } if ( parsedPercentMajorNav != null ) { monkeyTestRunner.setPercentMajorNav( parsedPercentMajorNav ); } if ( parsedPercentSyskeys != null ) { monkeyTestRunner.setPercentSyskeys( parsedPercentSyskeys ); } if ( parsedPercentAppswitch != null ) { monkeyTestRunner.setPercentAppswitch( parsedPercentAppswitch ); } if ( parsedPercentAnyevent != null ) { monkeyTestRunner.setPercentAnyEvent( parsedPercentAnyevent ); } if ( parsedPackages != null ) { monkeyTestRunner.setPackages( parsedPackages ); } if ( parsedCategories != null ) { monkeyTestRunner.setCategories( parsedCategories ); } monkeyTestRunner.setDebugNoEvents( parsedDebugNoEvents ); monkeyTestRunner.setHprof( parsedHprof ); monkeyTestRunner.setIgnoreCrashes( parsedIgnoreCrashes ); monkeyTestRunner.setIgnoreTimeouts( parsedIgnoreTimeouts ); monkeyTestRunner.setIgnoreSecurityExceptions( parsedIgnoreSecurityExceptions ); monkeyTestRunner.setKillProcessAfterError( parsedKillProcessAfterError ); monkeyTestRunner.setMonitorNativeCrash( parsedMonitorNativeCrashes ); getLog().info( deviceLogLinePrefix + "Running ui monkey tests" ); try { AndroidTestRunListener testRunListener = new AndroidTestRunListener( project, device ); monkeyTestRunner.run( testRunListener ); if ( testRunListener.hasFailuresOrErrors() && !isIgnoreTestFailures() ) { throw new MojoFailureException( deviceLogLinePrefix + "Tests failed on device." ); } if ( testRunListener.testRunFailed() ) { throw new MojoFailureException( deviceLogLinePrefix + "Test run failed to complete: " + testRunListener.getTestRunFailureCause() ); } if ( testRunListener.threwException() && !isIgnoreTestFailures() ) { throw new MojoFailureException( deviceLogLinePrefix + testRunListener.getExceptionMessages() ); } } catch ( TimeoutException e ) { throw new MojoExecutionException( deviceLogLinePrefix + "timeout", e ); } catch ( AdbCommandRejectedException e ) { throw new MojoExecutionException( deviceLogLinePrefix + "adb command rejected", e ); } catch ( ShellCommandUnresponsiveException e ) { throw new MojoExecutionException( deviceLogLinePrefix + "shell command " + "unresponsive", e ); } catch ( IOException e ) { throw new MojoExecutionException( deviceLogLinePrefix + "IO problem", e ); } } }; doWithDevices( instrumentationTestExecutor ); } /** * AndroidTestRunListener produces a nice output for the log for the test run as well as an xml file compatible with * the junit xml report file format understood by many tools. *

* It will do so for each device/emulator the tests run on. */ public class AndroidTestRunListener implements ITestRunListener { private static final String SCREENSHOT_SUFFIX = "_screenshot.png"; /** * the indent used in the log to group items that belong together visually * */ private static final String INDENT = " "; /** * Junit report schema documentation is sparse. Here are some hints * * @see "http://mail-archives.apache.org/mod_mbox/ ant-dev/200902.mbox/%3 * Cdffc72020902241548l4316d645w2e98caf5f0aac770 * @mail.gmail.com%3E" * @see "http://junitpdfreport.sourceforge.net/managedcontent/PdfTranslation" */ private static final String TAG_TESTSUITES = "testsuites"; private static final String TAG_TESTSUITE = "testsuite"; private static final String ATTR_TESTSUITE_ERRORS = "errors"; private static final String ATTR_TESTSUITE_FAILURES = "failures"; private static final String ATTR_TESTSUITE_HOSTNAME = "hostname"; private static final String ATTR_TESTSUITE_NAME = "name"; private static final String ATTR_TESTSUITE_TESTS = "tests"; private static final String ATTR_TESTSUITE_TIME = "time"; private static final String ATTR_TESTSUITE_TIMESTAMP = "timestamp"; private static final String TAG_PROPERTIES = "properties"; private static final String TAG_PROPERTY = "property"; private static final String ATTR_PROPERTY_NAME = "name"; private static final String ATTR_PROPERTY_VALUE = "value"; private static final String TAG_TESTCASE = "testcase"; private static final String ATTR_TESTCASE_NAME = "name"; private static final String ATTR_TESTCASE_CLASSNAME = "classname"; private static final String ATTR_TESTCASE_TIME = "time"; private static final String TAG_ERROR = "error"; private static final String TAG_FAILURE = "failure"; private static final String ATTR_MESSAGE = "message"; private static final String ATTR_TYPE = "type"; /** * time format for the output of milliseconds in seconds in the xml file * */ private final NumberFormat timeFormatter = new DecimalFormat( "#0.0000" ); private int testCount = 0; private int testRunCount = 0; private int testFailureCount = 0; private int testErrorCount = 0; private String testRunFailureCause = null; private final MavenProject project; /** * the emulator or device we are running the tests on * */ private final IDevice device; private final String deviceLogLinePrefix; // junit xml report related fields private Document junitReport; private Node testSuiteNode; /** * node for the current test case for junit report */ private Node currentTestCaseNode; /** * start time of current test case in millis, reset with each test start */ private long currentTestCaseStartTime; // we track if we have problems and then report upstream private boolean threwException = false; private final StringBuilder exceptionMessages = new StringBuilder(); /** * Create a new test run listener. * * @param project * the test project. * @param device * the device on which test is executed. */ public AndroidTestRunListener( MavenProject project, IDevice device ) { this.project = project; this.device = device; this.deviceLogLinePrefix = DeviceHelper.getDeviceLogLinePrefix( device ); } @Override public void testRunStarted( String runName, int tCount ) { this.testCount = tCount; getLog().info( deviceLogLinePrefix + INDENT + "Run started: " + runName + ", " + testCount + " tests:" ); if ( parsedCreateReport ) { try { DocumentBuilderFactory fact = DocumentBuilderFactory.newInstance(); DocumentBuilder parser = null; parser = fact.newDocumentBuilder(); junitReport = parser.newDocument(); Node testSuitesNode = junitReport.createElement( TAG_TESTSUITES ); junitReport.appendChild( testSuitesNode ); testSuiteNode = junitReport.createElement( TAG_TESTSUITE ); NamedNodeMap testSuiteAttributes = testSuiteNode.getAttributes(); Attr nameAttr = junitReport.createAttribute( ATTR_TESTSUITE_NAME ); nameAttr.setValue( runName ); testSuiteAttributes.setNamedItem( nameAttr ); Attr hostnameAttr = junitReport.createAttribute( ATTR_TESTSUITE_HOSTNAME ); hostnameAttr.setValue( DeviceHelper.getDescriptiveName( device ) ); testSuiteAttributes.setNamedItem( hostnameAttr ); Node propertiesNode = junitReport.createElement( TAG_PROPERTIES ); Node propertyNode; NamedNodeMap propertyAttributes; Attr propNameAttr; Attr propValueAttr; for ( Map.Entry< Object, Object > systemProperty : System.getProperties().entrySet() ) { propertyNode = junitReport.createElement( TAG_PROPERTY ); propertyAttributes = propertyNode.getAttributes(); propNameAttr = junitReport.createAttribute( ATTR_PROPERTY_NAME ); propNameAttr.setValue( systemProperty.getKey().toString() ); propertyAttributes.setNamedItem( propNameAttr ); propValueAttr = junitReport.createAttribute( ATTR_PROPERTY_VALUE ); propValueAttr.setValue( systemProperty.getValue().toString() ); propertyAttributes.setNamedItem( propValueAttr ); propertiesNode.appendChild( propertyNode ); } Map< String, String > deviceProperties = device.getProperties(); for ( Map.Entry< String, String > deviceProperty : deviceProperties.entrySet() ) { propertyNode = junitReport.createElement( TAG_PROPERTY ); propertyAttributes = propertyNode.getAttributes(); propNameAttr = junitReport.createAttribute( ATTR_PROPERTY_NAME ); propNameAttr.setValue( deviceProperty.getKey() ); propertyAttributes.setNamedItem( propNameAttr ); propValueAttr = junitReport.createAttribute( ATTR_PROPERTY_VALUE ); propValueAttr.setValue( deviceProperty.getValue() ); propertyAttributes.setNamedItem( propValueAttr ); propertiesNode.appendChild( propertyNode ); } testSuiteNode.appendChild( propertiesNode ); testSuitesNode.appendChild( testSuiteNode ); } catch ( ParserConfigurationException e ) { threwException = true; exceptionMessages.append( "Failed to create document" ); exceptionMessages.append( e.getMessage() ); } } } @Override public void testStarted( TestIdentifier testIdentifier ) { testRunCount++; getLog().info( deviceLogLinePrefix + String.format( "%1$s%1$sStart [%2$d/%3$d]: %4$s", INDENT, testRunCount, testCount, testIdentifier.toString() ) ); if ( parsedCreateReport ) { // reset start time for each test run currentTestCaseStartTime = new Date().getTime(); currentTestCaseNode = junitReport.createElement( TAG_TESTCASE ); NamedNodeMap testCaseAttributes = currentTestCaseNode.getAttributes(); Attr classAttr = junitReport.createAttribute( ATTR_TESTCASE_CLASSNAME ); classAttr.setValue( testIdentifier.getClassName() ); testCaseAttributes.setNamedItem( classAttr ); Attr methodAttr = junitReport.createAttribute( ATTR_TESTCASE_NAME ); methodAttr.setValue( testIdentifier.getTestName() ); testCaseAttributes.setNamedItem( methodAttr ); } } @Override public void testFailed( TestFailure status, TestIdentifier testIdentifier, String trace ) { if ( status == ERROR ) { ++testErrorCount; } else { ++testFailureCount; } getLog().info( deviceLogLinePrefix + INDENT + INDENT + status.name() + ":" + testIdentifier.toString() ); getLog().info( deviceLogLinePrefix + INDENT + INDENT + trace ); if ( parsedCreateReport ) { Node errorFailureNode; NamedNodeMap errorfailureAttributes; if ( status == ERROR ) { errorFailureNode = junitReport.createElement( TAG_ERROR ); errorfailureAttributes = errorFailureNode.getAttributes(); } else { errorFailureNode = junitReport.createElement( TAG_FAILURE ); errorfailureAttributes = errorFailureNode.getAttributes(); } errorFailureNode.setTextContent( trace ); Attr msgAttr = junitReport.createAttribute( ATTR_MESSAGE ); msgAttr.setValue( parseForMessage( trace ) ); errorfailureAttributes.setNamedItem( msgAttr ); Attr typeAttr = junitReport.createAttribute( ATTR_TYPE ); typeAttr.setValue( parseForException( trace ) ); errorfailureAttributes.setNamedItem( typeAttr ); currentTestCaseNode.appendChild( errorFailureNode ); } } private void executeOnAdbShell( String command ) { try { device.executeShellCommand( command, new IShellOutputReceiver() { @Override public boolean isCancelled() { return false; } @Override public void flush() { } @Override public void addOutput( byte[] data, int offset, int length ) { } } ); } catch ( TimeoutException e ) { getLog().error( e ); } catch ( AdbCommandRejectedException e ) { getLog().error( e ); } catch ( ShellCommandUnresponsiveException e ) { getLog().error( e ); } catch ( IOException e ) { getLog().error( e ); } } @Override public void testEnded( TestIdentifier testIdentifier, Map< String, String > testMetrics ) { getLog().info( deviceLogLinePrefix + String.format( "%1$s%1$sEnd [%2$d/%3$d]: %4$s", INDENT, testRunCount, testCount, testIdentifier.toString() ) ); logMetrics( testMetrics ); if ( parsedCreateReport ) { testSuiteNode.appendChild( currentTestCaseNode ); NamedNodeMap testCaseAttributes = currentTestCaseNode.getAttributes(); Attr timeAttr = junitReport.createAttribute( ATTR_TESTCASE_TIME ); long now = new Date().getTime(); double seconds = ( now - currentTestCaseStartTime ) / 1000.0; timeAttr.setValue( timeFormatter.format( seconds ) ); testCaseAttributes.setNamedItem( timeAttr ); } } @Override public void testRunEnded( long elapsedTime, Map< String, String > runMetrics ) { getLog().info( deviceLogLinePrefix + INDENT + "Run ended: " + elapsedTime + " ms" ); if ( hasFailuresOrErrors() ) { getLog().error( deviceLogLinePrefix + INDENT + "FAILURES!!!" ); } getLog().info( INDENT + "Tests run: " + testRunCount + ( testRunCount < testCount ? " (of " + testCount + ")" : "" ) + ", Failures: " + testFailureCount + ", Errors: " + testErrorCount ); if ( parsedCreateReport ) { NamedNodeMap testSuiteAttributes = testSuiteNode.getAttributes(); Attr testCountAttr = junitReport.createAttribute( ATTR_TESTSUITE_TESTS ); testCountAttr.setValue( Integer.toString( testCount ) ); testSuiteAttributes.setNamedItem( testCountAttr ); Attr testFailuresAttr = junitReport.createAttribute( ATTR_TESTSUITE_FAILURES ); testFailuresAttr.setValue( Integer.toString( testFailureCount ) ); testSuiteAttributes.setNamedItem( testFailuresAttr ); Attr testErrorsAttr = junitReport.createAttribute( ATTR_TESTSUITE_ERRORS ); testErrorsAttr.setValue( Integer.toString( testErrorCount ) ); testSuiteAttributes.setNamedItem( testErrorsAttr ); Attr timeAttr = junitReport.createAttribute( ATTR_TESTSUITE_TIME ); timeAttr.setValue( timeFormatter.format( elapsedTime / 1000.0 ) ); testSuiteAttributes.setNamedItem( timeAttr ); Attr timeStampAttr = junitReport.createAttribute( ATTR_TESTSUITE_TIMESTAMP ); timeStampAttr.setValue( new Date().toString() ); testSuiteAttributes.setNamedItem( timeStampAttr ); } logMetrics( runMetrics ); if ( parsedCreateReport ) { writeJunitReportToFile(); } } @Override public void testRunFailed( String errorMessage ) { testRunFailureCause = errorMessage; getLog().info( deviceLogLinePrefix + INDENT + "Run failed: " + errorMessage ); } @Override public void testRunStopped( long elapsedTime ) { getLog().info( deviceLogLinePrefix + INDENT + "Run stopped:" + elapsedTime ); } /** * Parse a trace string for the message in it. Assumes that the message is located after ":" and before "\r\n". * * @param trace * @return message or empty string */ private String parseForMessage( String trace ) { if ( StringUtils.isNotBlank( trace ) ) { String newline = "\r\n"; // if there is message like // junit.junit.framework.AssertionFailedError ... there is no // message int messageEnd = trace.indexOf( newline ); boolean hasMessage = !trace.startsWith( "junit." ) && messageEnd > 0; if ( hasMessage ) { int messageStart = trace.indexOf( ":" ) + 2; if ( messageStart > messageEnd ) { messageEnd = trace.indexOf( newline + "at" ); // match start of stack trace "\r\nat org.junit....." if ( messageStart > messageEnd ) { // ':' wasn't found in message but in stack trace messageStart = 0; } } return trace.substring( messageStart, messageEnd ); } else { return StringUtils.EMPTY; } } else { return StringUtils.EMPTY; } } /** * Parse a trace string for the exception class. Assumes that it is the start of the trace and ends at the first * ":". * * @param trace * @return Exception class as string or empty string */ private String parseForException( String trace ) { if ( StringUtils.isNotBlank( trace ) ) { return trace.substring( 0, trace.indexOf( ":" ) ); } else { return StringUtils.EMPTY; } } /** * Write the junit report xml file. */ private void writeJunitReportToFile() { TransformerFactory xfactory = TransformerFactory.newInstance(); Transformer xformer = null; try { xformer = xfactory.newTransformer(); } catch ( TransformerConfigurationException e ) { e.printStackTrace(); } Source source = new DOMSource( junitReport ); FileWriter writer = null; try { String directory = new StringBuilder().append( project.getBuild().getDirectory() ) .append( "/surefire-reports" ).toString(); FileUtils.forceMkdir( new File( directory ) ); String fileName = new StringBuilder().append( directory ).append( "/TEST-" ) .append( DeviceHelper.getDescriptiveName( device ) ).append( ".xml" ).toString(); File reportFile = new File( fileName ); writer = new FileWriter( reportFile ); Result result = new StreamResult( writer ); xformer.transform( source, result ); getLog().info( deviceLogLinePrefix + "Report file written to " + reportFile.getAbsolutePath() ); } catch ( IOException e ) { threwException = true; exceptionMessages.append( "Failed to write test report file" ); exceptionMessages.append( e.getMessage() ); } catch ( TransformerException e ) { threwException = true; exceptionMessages.append( "Failed to transform document to write to test report file" ); exceptionMessages.append( e.getMessage() ); } finally { IOUtils.closeQuietly( writer ); } } /** * Log all the metrics out in to key: value lines. * * @param metrics */ private void logMetrics( Map< String, String > metrics ) { for ( Map.Entry< String, String > entry : metrics.entrySet() ) { getLog().info( deviceLogLinePrefix + INDENT + INDENT + entry.getKey() + ": " + entry.getValue() ); } } /** * @return if any failures or errors occurred in the test run. */ public boolean hasFailuresOrErrors() { return testErrorCount > 0 || testFailureCount > 0; } /** * @return if the test run itself failed - a failure in the test infrastructure, not a test failure. */ public boolean testRunFailed() { return testRunFailureCause != null; } /** * @return the cause of test failure if any. */ public String getTestRunFailureCause() { return testRunFailureCause; } /** * @return if any exception was thrown during the test run on the build system (not the Android device or * emulator) */ public boolean threwException() { return threwException; } /** * @return all exception messages thrown during test execution on the test run time (not the Android device or * emulator) */ public String getExceptionMessages() { return exceptionMessages.toString(); } } /** * @return default seed. */ // used via PullParameter annotation - do not remove private Long getSeed() { return parsedSeed; } /** * @return default throttle. */ // used via PullParameter annotation - do not remove private Long getThrottle() { return parsedThrottle; } /** * @return default percentTouch. */ // used via PullParameter annotation - do not remove private Integer getPercentTouch() { return parsedPercentTouch; } /** * @return default percentMotion. */ // used via PullParameter annotation - do not remove private Integer getPercentMotion() { return parsedPercentMotion; } /** * @return default percentTrackball. */ // used via PullParameter annotation - do not remove private Integer getPercentTrackball() { return parsedPercentTrackball; } /** * @return default percentNav. */ // used via PullParameter annotation - do not remove private Integer getPercentNav() { return parsedPercentNav; } /** * @return default percentMajorNav. */ // used via PullParameter annotation - do not remove private Integer getPercentMajorNav() { return parsedPercentMajorNav; } /** * @return default percentSyskeys. */ // used via PullParameter annotation - do not remove private Integer getPercentSyskeys() { return parsedPercentSyskeys; } /** * @return default percentAppSwitch. */ // used via PullParameter annotation - do not remove private Integer getPercentAppswitch() { return parsedPercentAppswitch; } /** * @return default percentAnyEvent. */ // used via PullParameter annotation - do not remove private Integer getPercentAnyevent() { return parsedPercentAnyevent; } /** * @return default packages. */ public String[] getPackages() { return parsedPackages; } /** * @return default categories. */ public String[] getCategories() { return parsedCategories; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy