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