Home | History | Annotate | Download | only in runner
      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.test.runner;
     18 
     19 import android.app.Activity;
     20 import android.app.Instrumentation;
     21 import android.os.Bundle;
     22 import android.os.Debug;
     23 import android.os.Looper;
     24 import android.test.suitebuilder.annotation.LargeTest;
     25 import android.util.Log;
     26 
     27 import com.android.test.runner.listener.CoverageListener;
     28 import com.android.test.runner.listener.DelayInjector;
     29 import com.android.test.runner.listener.InstrumentationResultPrinter;
     30 import com.android.test.runner.listener.InstrumentationRunListener;
     31 import com.android.test.runner.listener.SuiteAssignmentPrinter;
     32 
     33 import org.junit.internal.TextListener;
     34 import org.junit.runner.JUnitCore;
     35 import org.junit.runner.Result;
     36 import org.junit.runner.notification.RunListener;
     37 
     38 import java.io.ByteArrayOutputStream;
     39 import java.io.PrintStream;
     40 import java.util.ArrayList;
     41 import java.util.List;
     42 
     43 /**
     44  * An {@link Instrumentation} that runs JUnit3 and JUnit4 tests against
     45  * an Android package (application).
     46  * <p/>
     47  * Currently experimental. Based on {@link android.test.InstrumentationTestRunner}.
     48  * <p/>
     49  * Will eventually support a superset of {@link android.test.InstrumentationTestRunner} features,
     50  * while maintaining command/output format compatibility with that class.
     51  *
     52  * <h3>Typical Usage</h3>
     53  * <p/>
     54  * Write JUnit3 style {@link junit.framework.TestCase}s and/or JUnit4 style
     55  * {@link org.junit.Test}s that perform tests against the classes in your package.
     56  * Make use of the {@link com.android.test.InjectContext} and
     57  * {@link com.android.test.InjectInstrumentation} annotations if needed.
     58  * <p/>
     59  * In an appropriate AndroidManifest.xml, define an instrumentation with android:name set to
     60  * {@link com.android.test.runner.AndroidJUnitRunner} and the appropriate android:targetPackage set.
     61  * <p/>
     62  * Execution options:
     63  * <p/>
     64  * <b>Running all tests:</b> adb shell am instrument -w
     65  * com.android.foo/com.android.test.runner.AndroidJUnitRunner
     66  * <p/>
     67  * <b>Running all tests in a class:</b> adb shell am instrument -w
     68  * -e class com.android.foo.FooTest
     69  * com.android.foo/com.android.test.runner.AndroidJUnitRunner
     70  * <p/>
     71  * <b>Running a single test:</b> adb shell am instrument -w
     72  * -e class com.android.foo.FooTest#testFoo
     73  * com.android.foo/com.android.test.runner.AndroidJUnitRunner
     74  * <p/>
     75  * <b>Running all tests in multiple classes:</b> adb shell am instrument -w
     76  * -e class com.android.foo.FooTest,com.android.foo.TooTest
     77  * com.android.foo/com.android.test.runner.AndroidJUnitRunner
     78  * <p/>
     79  * <b>Running all tests in a java package:</b> adb shell am instrument -w
     80  * -e package com.android.foo.bar
     81  * com.android.foo/com.android.test.runner.AndroidJUnitRunner
     82  * <b>To debug your tests, set a break point in your code and pass:</b>
     83  * -e debug true
     84  * <p/>
     85  * <b>Running a specific test size i.e. annotated with
     86  * {@link android.test.suitebuilder.annotation.SmallTest} or
     87  * {@link android.test.suitebuilder.annotation.MediumTest} or
     88  * {@link android.test.suitebuilder.annotation.LargeTest}:</b>
     89  * adb shell am instrument -w -e size [small|medium|large]
     90  * com.android.foo/android.test.InstrumentationTestRunner
     91  * <p/>
     92  * <b>Filter test run to tests with given annotation:</b> adb shell am instrument -w
     93  * -e annotation com.android.foo.MyAnnotation
     94  * com.android.foo/android.test.InstrumentationTestRunner
     95  * <p/>
     96  * If used with other options, the resulting test run will contain the intersection of the two
     97  * options.
     98  * e.g. "-e size large -e annotation com.android.foo.MyAnnotation" will run only tests with both
     99  * the {@link LargeTest} and "com.android.foo.MyAnnotation" annotations.
    100  * <p/>
    101  * <b>Filter test run to tests <i>without</i> given annotation:</b> adb shell am instrument -w
    102  * -e notAnnotation com.android.foo.MyAnnotation
    103  * com.android.foo/android.test.InstrumentationTestRunner
    104  * <p/>
    105  * As above, if used with other options, the resulting test run will contain the intersection of
    106  * the two options.
    107  * e.g. "-e size large -e notAnnotation com.android.foo.MyAnnotation" will run tests with
    108  * the {@link LargeTest} annotation that do NOT have the "com.android.foo.MyAnnotation" annotations.
    109  * <p/>
    110  * <b>To run in 'log only' mode</b>
    111  * -e log true
    112  * This option will load and iterate through all test classes and methods, but will bypass actual
    113  * test execution. Useful for quickly obtaining info on the tests to be executed by an
    114  * instrumentation command.
    115  * <p/>
    116  * <b>To generate EMMA code coverage:</b>
    117  * -e coverage true
    118  * Note: this requires an emma instrumented build. By default, the code coverage results file
    119  * will be saved in a /data/<app>/coverage.ec file, unless overridden by coverageFile flag (see
    120  * below)
    121  * <p/>
    122  * <b> To specify EMMA code coverage results file path:</b>
    123  * -e coverageFile /sdcard/myFile.ec
    124  * <p/>
    125  */
    126 public class AndroidJUnitRunner extends Instrumentation {
    127 
    128     // constants for supported instrumentation arguments
    129     public static final String ARGUMENT_TEST_CLASS = "class";
    130     private static final String ARGUMENT_TEST_SIZE = "size";
    131     private static final String ARGUMENT_LOG_ONLY = "log";
    132     private static final String ARGUMENT_ANNOTATION = "annotation";
    133     private static final String ARGUMENT_NOT_ANNOTATION = "notAnnotation";
    134     private static final String ARGUMENT_DELAY_MSEC = "delay_msec";
    135     private static final String ARGUMENT_COVERAGE = "coverage";
    136     private static final String ARGUMENT_COVERAGE_PATH = "coverageFile";
    137     private static final String ARGUMENT_SUITE_ASSIGNMENT = "suiteAssignment";
    138     private static final String ARGUMENT_DEBUG = "debug";
    139     private static final String ARGUMENT_EXTRA_LISTENER = "extraListener";
    140     private static final String ARGUMENT_TEST_PACKAGE = "package";
    141     // TODO: consider supporting 'count' from InstrumentationTestRunner
    142 
    143     private static final String LOG_TAG = "AndroidJUnitRunner";
    144 
    145     private Bundle mArguments;
    146 
    147     @Override
    148     public void onCreate(Bundle arguments) {
    149         super.onCreate(arguments);
    150         mArguments = arguments;
    151 
    152         start();
    153     }
    154 
    155     /**
    156      * Get the Bundle object that contains the arguments passed to the instrumentation
    157      *
    158      * @return the Bundle object
    159      * @hide
    160      */
    161     public Bundle getArguments(){
    162         return mArguments;
    163     }
    164 
    165     /**
    166      * Set the arguments.
    167      *
    168      * @VisibleForTesting
    169      */
    170     void setArguments(Bundle args) {
    171         mArguments = args;
    172     }
    173 
    174     private boolean getBooleanArgument(String tag) {
    175         String tagString = getArguments().getString(tag);
    176         return tagString != null && Boolean.parseBoolean(tagString);
    177     }
    178 
    179     /**
    180      * Initialize the current thread as a looper.
    181      * <p/>
    182      * Exposed for unit testing.
    183      */
    184     void prepareLooper() {
    185         Looper.prepare();
    186     }
    187 
    188     @Override
    189     public void onStart() {
    190         prepareLooper();
    191 
    192         if (getBooleanArgument(ARGUMENT_DEBUG)) {
    193             Debug.waitForDebugger();
    194         }
    195 
    196         setupDexmaker();
    197 
    198         ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    199         PrintStream writer = new PrintStream(byteArrayOutputStream);
    200         List<RunListener> listeners = new ArrayList<RunListener>();
    201 
    202         try {
    203             JUnitCore testRunner = new JUnitCore();
    204             addListeners(listeners, testRunner, writer);
    205 
    206             TestRequest testRequest = buildRequest(getArguments(), writer);
    207             Result result = testRunner.run(testRequest.getRequest());
    208             result.getFailures().addAll(testRequest.getFailures());
    209             Log.i(LOG_TAG, String.format("Test run complete. %d tests, %d failed, %d ignored",
    210                     result.getRunCount(), result.getFailureCount(), result.getIgnoreCount()));
    211         } catch (Throwable t) {
    212             // catch all exceptions so a more verbose error message can be displayed
    213             writer.println(String.format(
    214                     "Test run aborted due to unexpected exception: %s",
    215                     t.getMessage()));
    216             t.printStackTrace(writer);
    217 
    218         } finally {
    219             Bundle results = new Bundle();
    220             reportRunEnded(listeners, writer, results);
    221             writer.close();
    222             results.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
    223                     String.format("\n%s",
    224                             byteArrayOutputStream.toString()));
    225             finish(Activity.RESULT_OK, results);
    226         }
    227 
    228     }
    229 
    230     private void addListeners(List<RunListener> listeners, JUnitCore testRunner,
    231             PrintStream writer) {
    232         if (getBooleanArgument(ARGUMENT_SUITE_ASSIGNMENT)) {
    233             addListener(listeners, testRunner, new SuiteAssignmentPrinter(writer));
    234         } else {
    235             addListener(listeners, testRunner, new TextListener(writer));
    236             addListener(listeners, testRunner, new InstrumentationResultPrinter(this));
    237             addDelayListener(listeners, testRunner);
    238             addCoverageListener(listeners, testRunner);
    239         }
    240 
    241         addExtraListeners(listeners, testRunner, writer);
    242     }
    243 
    244     private void addListener(List<RunListener> list, JUnitCore testRunner, RunListener listener) {
    245         list.add(listener);
    246         testRunner.addListener(listener);
    247     }
    248 
    249     private void addCoverageListener(List<RunListener> list, JUnitCore testRunner) {
    250         if (getBooleanArgument(ARGUMENT_COVERAGE)) {
    251             String coverageFilePath = getArguments().getString(ARGUMENT_COVERAGE_PATH);
    252             addListener(list, testRunner, new CoverageListener(this, coverageFilePath));
    253         }
    254     }
    255 
    256     /**
    257      * Sets up listener to inject {@link #ARGUMENT_DELAY_MSEC}, if specified.
    258      * @param testRunner
    259      */
    260     private void addDelayListener(List<RunListener> list, JUnitCore testRunner) {
    261         try {
    262             Object delay = getArguments().get(ARGUMENT_DELAY_MSEC);  // Accept either string or int
    263             if (delay != null) {
    264                 int delayMsec = Integer.parseInt(delay.toString());
    265                 addListener(list, testRunner, new DelayInjector(delayMsec));
    266             }
    267         } catch (NumberFormatException e) {
    268             Log.e(LOG_TAG, "Invalid delay_msec parameter", e);
    269         }
    270     }
    271 
    272     private void addExtraListeners(List<RunListener> listeners, JUnitCore testRunner,
    273             PrintStream writer) {
    274         String extraListenerList = getArguments().getString(ARGUMENT_EXTRA_LISTENER);
    275         if (extraListenerList == null) {
    276             return;
    277         }
    278 
    279         for (String listenerName : extraListenerList.split(",")) {
    280             addExtraListener(listeners, testRunner, writer, listenerName);
    281         }
    282     }
    283 
    284     private void addExtraListener(List<RunListener> listeners, JUnitCore testRunner,
    285             PrintStream writer, String extraListener) {
    286         if (extraListener == null || extraListener.length() == 0) {
    287             return;
    288         }
    289 
    290         final Class<?> klass;
    291         try {
    292             klass = Class.forName(extraListener);
    293         } catch (ClassNotFoundException e) {
    294             writer.println("Could not find extra RunListener class " + extraListener);
    295             return;
    296         }
    297 
    298         if (!RunListener.class.isAssignableFrom(klass)) {
    299             writer.println("Extra listeners must extend RunListener class " + extraListener);
    300             return;
    301         }
    302 
    303         try {
    304             klass.getConstructor().setAccessible(true);
    305         } catch (NoSuchMethodException e) {
    306             writer.println("Must have no argument constructor for class " + extraListener);
    307             return;
    308         }
    309 
    310         final RunListener l;
    311         try {
    312             l = (RunListener) klass.newInstance();
    313         } catch (Throwable t) {
    314             writer.println("Could not instantiate extra RunListener class " + extraListener);
    315             t.printStackTrace(writer);
    316             return;
    317         }
    318 
    319         addListener(listeners, testRunner, l);
    320     }
    321 
    322     private void reportRunEnded(List<RunListener> listeners, PrintStream writer, Bundle results) {
    323         for (RunListener listener : listeners) {
    324             if (listener instanceof InstrumentationRunListener) {
    325                 ((InstrumentationRunListener)listener).instrumentationRunFinished(writer, results);
    326             }
    327         }
    328     }
    329 
    330     /**
    331      * Builds a {@link TestRequest} based on given input arguments.
    332      * <p/>
    333      * Exposed for unit testing.
    334      */
    335     TestRequest buildRequest(Bundle arguments, PrintStream writer) {
    336         // only load tests for current aka testContext
    337         // Note that this represents a change from InstrumentationTestRunner where
    338         // getTargetContext().getPackageCodePath() was also scanned
    339         TestRequestBuilder builder = createTestRequestBuilder(writer,
    340                 getContext().getPackageCodePath());
    341 
    342         String testClassName = arguments.getString(ARGUMENT_TEST_CLASS);
    343         if (testClassName != null) {
    344             for (String className : testClassName.split(",")) {
    345                 parseTestClass(className, builder);
    346             }
    347         }
    348 
    349         String testPackage = arguments.getString(ARGUMENT_TEST_PACKAGE);
    350         if (testPackage != null) {
    351             builder.addTestPackageFilter(testPackage);
    352         }
    353 
    354         String testSize = arguments.getString(ARGUMENT_TEST_SIZE);
    355         if (testSize != null) {
    356             builder.addTestSizeFilter(testSize);
    357         }
    358 
    359         String annotation = arguments.getString(ARGUMENT_ANNOTATION);
    360         if (annotation != null) {
    361             builder.addAnnotationInclusionFilter(annotation);
    362         }
    363 
    364         String notAnnotation = arguments.getString(ARGUMENT_NOT_ANNOTATION);
    365         if (notAnnotation != null) {
    366             builder.addAnnotationExclusionFilter(notAnnotation);
    367         }
    368 
    369         if (getBooleanArgument(ARGUMENT_LOG_ONLY)) {
    370             builder.setSkipExecution(true);
    371         }
    372         return builder.build(this, arguments);
    373     }
    374 
    375     /**
    376      * Factory method for {@link TestRequestBuilder}.
    377      * <p/>
    378      * Exposed for unit testing.
    379      */
    380     TestRequestBuilder createTestRequestBuilder(PrintStream writer, String... packageCodePaths) {
    381         return new TestRequestBuilder(writer, packageCodePaths);
    382     }
    383 
    384     /**
    385      * Parse and load the given test class and, optionally, method
    386      *
    387      * @param testClassName - full package name of test class and optionally method to add.
    388      *        Expected format: com.android.TestClass#testMethod
    389      * @param testSuiteBuilder - builder to add tests to
    390      */
    391     private void parseTestClass(String testClassName, TestRequestBuilder testRequestBuilder) {
    392         int methodSeparatorIndex = testClassName.indexOf('#');
    393 
    394         if (methodSeparatorIndex > 0) {
    395             String testMethodName = testClassName.substring(methodSeparatorIndex + 1);
    396             testClassName = testClassName.substring(0, methodSeparatorIndex);
    397             testRequestBuilder.addTestMethod(testClassName, testMethodName);
    398         } else {
    399             testRequestBuilder.addTestClass(testClassName);
    400         }
    401     }
    402 
    403     private void setupDexmaker() {
    404         // Explicitly set the Dexmaker cache, so tests that use mocking frameworks work
    405         String dexCache = getTargetContext().getCacheDir().getPath();
    406         Log.i(LOG_TAG, "Setting dexmaker.dexcache to " + dexCache);
    407         System.setProperty("dexmaker.dexcache", getTargetContext().getCacheDir().getPath());
    408     }
    409 }
    410