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
17package com.android.test.runner;
18
19import android.app.Activity;
20import android.app.Instrumentation;
21import android.os.Bundle;
22import android.os.Debug;
23import android.os.Looper;
24import android.test.suitebuilder.annotation.LargeTest;
25import android.util.Log;
26
27import com.android.test.runner.listener.CoverageListener;
28import com.android.test.runner.listener.DelayInjector;
29import com.android.test.runner.listener.InstrumentationResultPrinter;
30import com.android.test.runner.listener.InstrumentationRunListener;
31import com.android.test.runner.listener.SuiteAssignmentPrinter;
32
33import org.junit.internal.TextListener;
34import org.junit.runner.JUnitCore;
35import org.junit.runner.Result;
36import org.junit.runner.notification.RunListener;
37
38import java.io.ByteArrayOutputStream;
39import java.io.PrintStream;
40import java.util.ArrayList;
41import 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 */
126public 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