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 org.junit.internal.TextListener;
28import org.junit.runner.Description;
29import org.junit.runner.JUnitCore;
30import org.junit.runner.Result;
31import org.junit.runner.notification.Failure;
32import org.junit.runner.notification.RunListener;
33
34import java.io.ByteArrayOutputStream;
35import java.io.PrintStream;
36
37/**
38 * An {@link Instrumentation} that runs JUnit3 and JUnit4 tests against
39 * an Android package (application).
40 * <p/>
41 * Currently experimental. Based on {@link android.test.InstrumentationTestRunner}.
42 * <p/>
43 * Will eventually support a superset of {@link android.test.InstrumentationTestRunner} features,
44 * while maintaining command/output format compatibility with that class.
45 *
46 * <h3>Typical Usage</h3>
47 * <p/>
48 * Write JUnit3 style {@link junit.framework.TestCase}s and/or JUnit4 style
49 * {@link org.junit.Test}s that perform tests against the classes in your package.
50 * Make use of the {@link com.android.test.InjectContext} and
51 * {@link com.android.test.InjectInstrumentation} annotations if needed.
52 * <p/>
53 * In an appropriate AndroidManifest.xml, define an instrumentation with android:name set to
54 * {@link com.android.test.runner.AndroidJUnitRunner} and the appropriate android:targetPackage set.
55 * <p/>
56 * Execution options:
57 * <p/>
58 * <b>Running all tests:</b> adb shell am instrument -w
59 * com.android.foo/com.android.test.runner.AndroidJUnitRunner
60 * <p/>
61 * <b>Running all tests in a class:</b> adb shell am instrument -w
62 * -e class com.android.foo.FooTest
63 * com.android.foo/com.android.test.runner.AndroidJUnitRunner
64 * <p/>
65 * <b>Running a single test:</b> adb shell am instrument -w
66 * -e class com.android.foo.FooTest#testFoo
67 * com.android.foo/com.android.test.runner.AndroidJUnitRunner
68 * <p/>
69 * <b>Running all tests in multiple classes:</b> adb shell am instrument -w
70 * -e class com.android.foo.FooTest,com.android.foo.TooTest
71 * com.android.foo/com.android.test.runner.AndroidJUnitRunner
72 * <p/>
73 * <b>To debug your tests, set a break point in your code and pass:</b>
74 * -e debug true
75 * <p/>
76 * <b>Running a specific test size i.e. annotated with
77 * {@link android.test.suitebuilder.annotation.SmallTest} or
78 * {@link android.test.suitebuilder.annotation.MediumTest} or
79 * {@link android.test.suitebuilder.annotation.LargeTest}:</b>
80 * adb shell am instrument -w -e size [small|medium|large]
81 * com.android.foo/android.test.InstrumentationTestRunner
82 * <p/>
83 * <b>Filter test run to tests with given annotation:</b> adb shell am instrument -w
84 * -e annotation com.android.foo.MyAnnotation
85 * com.android.foo/android.test.InstrumentationTestRunner
86 * <p/>
87 * If used with other options, the resulting test run will contain the intersection of the two
88 * options.
89 * e.g. "-e size large -e annotation com.android.foo.MyAnnotation" will run only tests with both
90 * the {@link LargeTest} and "com.android.foo.MyAnnotation" annotations.
91 * <p/>
92 * <b>Filter test run to tests <i>without</i> given annotation:</b> adb shell am instrument -w
93 * -e notAnnotation com.android.foo.MyAnnotation
94 * com.android.foo/android.test.InstrumentationTestRunner
95 * <p/>
96 * As above, if used with other options, the resulting test run will contain the intersection of
97 * the two options.
98 * e.g. "-e size large -e notAnnotation com.android.foo.MyAnnotation" will run tests with
99 * the {@link LargeTest} annotation that do NOT have the "com.android.foo.MyAnnotation" annotations.
100 * <p/>
101 * <b>To run in 'log only' mode</b>
102 * -e log true
103 * This option will load and iterate through all test classes and methods, but will bypass actual
104 * test execution. Useful for quickly obtaining info on the tests to be executed by an
105 * instrumentation command.
106 * <p/>
107 */
108public class AndroidJUnitRunner extends Instrumentation {
109
110    public static final String ARGUMENT_TEST_CLASS = "class";
111
112    private static final String ARGUMENT_TEST_SIZE = "size";
113    private static final String ARGUMENT_LOG_ONLY = "log";
114    private static final String ARGUMENT_ANNOTATION = "annotation";
115    private static final String ARGUMENT_NOT_ANNOTATION = "notAnnotation";
116
117    /**
118     * The following keys are used in the status bundle to provide structured reports to
119     * an IInstrumentationWatcher.
120     */
121
122    /**
123     * This value, if stored with key {@link android.app.Instrumentation#REPORT_KEY_IDENTIFIER},
124     * identifies InstrumentationTestRunner as the source of the report.  This is sent with all
125     * status messages.
126     */
127    public static final String REPORT_VALUE_ID = "InstrumentationTestRunner";
128    /**
129     * If included in the status or final bundle sent to an IInstrumentationWatcher, this key
130     * identifies the total number of tests that are being run.  This is sent with all status
131     * messages.
132     */
133    public static final String REPORT_KEY_NUM_TOTAL = "numtests";
134    /**
135     * If included in the status or final bundle sent to an IInstrumentationWatcher, this key
136     * identifies the sequence number of the current test.  This is sent with any status message
137     * describing a specific test being started or completed.
138     */
139    public static final String REPORT_KEY_NUM_CURRENT = "current";
140    /**
141     * If included in the status or final bundle sent to an IInstrumentationWatcher, this key
142     * identifies the name of the current test class.  This is sent with any status message
143     * describing a specific test being started or completed.
144     */
145    public static final String REPORT_KEY_NAME_CLASS = "class";
146    /**
147     * If included in the status or final bundle sent to an IInstrumentationWatcher, this key
148     * identifies the name of the current test.  This is sent with any status message
149     * describing a specific test being started or completed.
150     */
151    public static final String REPORT_KEY_NAME_TEST = "test";
152
153    /**
154     * The test is starting.
155     */
156    public static final int REPORT_VALUE_RESULT_START = 1;
157    /**
158     * The test completed successfully.
159     */
160    public static final int REPORT_VALUE_RESULT_OK = 0;
161    /**
162     * The test completed with an error.
163     */
164    public static final int REPORT_VALUE_RESULT_ERROR = -1;
165    /**
166     * The test completed with a failure.
167     */
168    public static final int REPORT_VALUE_RESULT_FAILURE = -2;
169    /**
170     * The test was ignored.
171     */
172    public static final int REPORT_VALUE_RESULT_IGNORED = -3;
173    /**
174     * If included in the status bundle sent to an IInstrumentationWatcher, this key
175     * identifies a stack trace describing an error or failure.  This is sent with any status
176     * message describing a specific test being completed.
177     */
178    public static final String REPORT_KEY_STACK = "stack";
179
180    private static final String LOG_TAG = "InstrumentationTestRunner";
181
182    private final Bundle mResults = new Bundle();
183    private Bundle mArguments;
184
185    @Override
186    public void onCreate(Bundle arguments) {
187        super.onCreate(arguments);
188        mArguments = arguments;
189
190        start();
191    }
192
193    /**
194     * Get the Bundle object that contains the arguments passed to the instrumentation
195     *
196     * @return the Bundle object
197     * @hide
198     */
199    public Bundle getArguments(){
200        return mArguments;
201    }
202
203    private boolean getBooleanArgument(Bundle arguments, String tag) {
204        String tagString = arguments.getString(tag);
205        return tagString != null && Boolean.parseBoolean(tagString);
206    }
207
208    /**
209     * Initialize the current thread as a looper.
210     * <p/>
211     * Exposed for unit testing.
212     */
213    void prepareLooper() {
214        Looper.prepare();
215    }
216
217    @Override
218    public void onStart() {
219        prepareLooper();
220
221        if (getBooleanArgument(getArguments(), "debug")) {
222            Debug.waitForDebugger();
223        }
224
225        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
226        PrintStream writer = new PrintStream(byteArrayOutputStream);
227        try {
228            JUnitCore testRunner = new JUnitCore();
229            testRunner.addListener(new TextListener(writer));
230            WatcherResultPrinter detailedResultPrinter = new WatcherResultPrinter();
231            testRunner.addListener(detailedResultPrinter);
232
233            TestRequest testRequest = buildRequest(getArguments(), writer);
234            Result result = testRunner.run(testRequest.getRequest());
235            result.getFailures().addAll(testRequest.getFailures());
236            Log.i(LOG_TAG, String.format("Test run complete. %d tests, %d failed, %d ignored",
237                    result.getRunCount(), result.getFailureCount(), result.getIgnoreCount()));
238        } catch (Throwable t) {
239            // catch all exceptions so a more verbose error message can be displayed
240            writer.println(String.format(
241                    "Test run aborted due to unexpected exception: %s",
242                    t.getMessage()));
243            t.printStackTrace(writer);
244
245        } finally {
246            writer.close();
247            mResults.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
248                    String.format("\n%s",
249                            byteArrayOutputStream.toString()));
250            finish(Activity.RESULT_OK, mResults);
251        }
252
253    }
254
255    /**
256     * Builds a {@link TestRequest} based on given input arguments.
257     * <p/>
258     * Exposed for unit testing.
259     */
260    TestRequest buildRequest(Bundle arguments, PrintStream writer) {
261        // only load tests for current aka testContext
262        // Note that this represents a change from InstrumentationTestRunner where
263        // getTargetContext().getPackageCodePath() was also scanned
264        TestRequestBuilder builder = createTestRequestBuilder(writer,
265                getContext().getPackageCodePath());
266
267        String testClassName = arguments.getString(ARGUMENT_TEST_CLASS);
268        if (testClassName != null) {
269            for (String className : testClassName.split(",")) {
270                parseTestClass(className, builder);
271            }
272        }
273
274        String testSize = arguments.getString(ARGUMENT_TEST_SIZE);
275        if (testSize != null) {
276            builder.addTestSizeFilter(testSize);
277        }
278
279        String annotation = arguments.getString(ARGUMENT_ANNOTATION);
280        if (annotation != null) {
281            builder.addAnnotationInclusionFilter(annotation);
282        }
283
284        String notAnnotation = arguments.getString(ARGUMENT_NOT_ANNOTATION);
285        if (notAnnotation != null) {
286            builder.addAnnotationExclusionFilter(notAnnotation);
287        }
288
289        boolean logOnly = getBooleanArgument(arguments, ARGUMENT_LOG_ONLY);
290        if (logOnly) {
291            builder.setSkipExecution(true);
292        }
293        return builder.build(this);
294    }
295
296    /**
297     * Factory method for {@link TestRequestBuilder}.
298     * <p/>
299     * Exposed for unit testing.
300     */
301    TestRequestBuilder createTestRequestBuilder(PrintStream writer, String... packageCodePaths) {
302        return new TestRequestBuilder(writer, packageCodePaths);
303    }
304
305    /**
306     * Parse and load the given test class and, optionally, method
307     *
308     * @param testClassName - full package name of test class and optionally method to add.
309     *        Expected format: com.android.TestClass#testMethod
310     * @param testSuiteBuilder - builder to add tests to
311     */
312    private void parseTestClass(String testClassName, TestRequestBuilder testRequestBuilder) {
313        int methodSeparatorIndex = testClassName.indexOf('#');
314
315        if (methodSeparatorIndex > 0) {
316            String testMethodName = testClassName.substring(methodSeparatorIndex + 1);
317            testClassName = testClassName.substring(0, methodSeparatorIndex);
318            testRequestBuilder.addTestMethod(testClassName, testMethodName);
319        } else {
320            testRequestBuilder.addTestClass(testClassName);
321        }
322    }
323
324    /**
325     * This class sends status reports back to the IInstrumentationWatcher
326     */
327    private class WatcherResultPrinter extends RunListener {
328        private final Bundle mResultTemplate;
329        Bundle mTestResult;
330        int mTestNum = 0;
331        int mTestResultCode = 0;
332        String mTestClass = null;
333
334        public WatcherResultPrinter() {
335            mResultTemplate = new Bundle();
336        }
337
338        @Override
339        public void testRunStarted(Description description) throws Exception {
340            mResultTemplate.putString(Instrumentation.REPORT_KEY_IDENTIFIER, REPORT_VALUE_ID);
341            mResultTemplate.putInt(REPORT_KEY_NUM_TOTAL, description.testCount());
342        }
343
344        @Override
345        public void testRunFinished(Result result) throws Exception {
346            // TODO: implement this
347        }
348
349        /**
350         * send a status for the start of a each test, so long tests can be seen
351         * as "running"
352         */
353        @Override
354        public void testStarted(Description description) throws Exception {
355            String testClass = description.getClassName();
356            String testName = description.getMethodName();
357            mTestResult = new Bundle(mResultTemplate);
358            mTestResult.putString(REPORT_KEY_NAME_CLASS, testClass);
359            mTestResult.putString(REPORT_KEY_NAME_TEST, testName);
360            mTestResult.putInt(REPORT_KEY_NUM_CURRENT, ++mTestNum);
361            // pretty printing
362            if (testClass != null && !testClass.equals(mTestClass)) {
363                mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
364                        String.format("\n%s:", testClass));
365                mTestClass = testClass;
366            } else {
367                mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, "");
368            }
369
370            sendStatus(REPORT_VALUE_RESULT_START, mTestResult);
371            mTestResultCode = 0;
372        }
373
374        @Override
375        public void testFinished(Description description) throws Exception {
376            if (mTestResultCode == 0) {
377                mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, ".");
378            }
379            sendStatus(mTestResultCode, mTestResult);
380        }
381
382        @Override
383        public void testFailure(Failure failure) throws Exception {
384            mTestResultCode = REPORT_VALUE_RESULT_ERROR;
385            reportFailure(failure);
386        }
387
388
389        @Override
390        public void testAssumptionFailure(Failure failure) {
391            mTestResultCode = REPORT_VALUE_RESULT_FAILURE;
392            reportFailure(failure);
393        }
394
395        private void reportFailure(Failure failure) {
396            mTestResult.putString(REPORT_KEY_STACK, failure.getTrace());
397            // pretty printing
398            mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
399                    String.format("\nError in %s:\n%s",
400                            failure.getDescription().getDisplayName(), failure.getTrace()));
401        }
402
403        @Override
404        public void testIgnored(Description description) throws Exception {
405            testStarted(description);
406            mTestResultCode = REPORT_VALUE_RESULT_IGNORED;
407            testFinished(description);
408        }
409    }
410}
411