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