Reconcile with jb-mr1.1-release - do not merge

Change-Id: Ib4c90d47d0d64db2c116a47f026fb8a4c78461f9
diff --git a/androidtestlib/src/com/android/test/runner/AndroidJUnitRunner.java b/androidtestlib/src/com/android/test/runner/AndroidJUnitRunner.java
index ddab2b6..a932523 100644
--- a/androidtestlib/src/com/android/test/runner/AndroidJUnitRunner.java
+++ b/androidtestlib/src/com/android/test/runner/AndroidJUnitRunner.java
@@ -24,15 +24,21 @@
 import android.test.suitebuilder.annotation.LargeTest;
 import android.util.Log;
 
+import com.android.test.runner.listener.CoverageListener;
+import com.android.test.runner.listener.DelayInjector;
+import com.android.test.runner.listener.InstrumentationResultPrinter;
+import com.android.test.runner.listener.InstrumentationRunListener;
+import com.android.test.runner.listener.SuiteAssignmentPrinter;
+
 import org.junit.internal.TextListener;
-import org.junit.runner.Description;
 import org.junit.runner.JUnitCore;
 import org.junit.runner.Result;
-import org.junit.runner.notification.Failure;
 import org.junit.runner.notification.RunListener;
 
 import java.io.ByteArrayOutputStream;
 import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * An {@link Instrumentation} that runs JUnit3 and JUnit4 tests against
@@ -104,82 +110,33 @@
  * test execution. Useful for quickly obtaining info on the tests to be executed by an
  * instrumentation command.
  * <p/>
+ * <b>To generate EMMA code coverage:</b>
+ * -e coverage true
+ * Note: this requires an emma instrumented build. By default, the code coverage results file
+ * will be saved in a /data/<app>/coverage.ec file, unless overridden by coverageFile flag (see
+ * below)
+ * <p/>
+ * <b> To specify EMMA code coverage results file path:</b>
+ * -e coverageFile /sdcard/myFile.ec
+ * <p/>
  */
 public class AndroidJUnitRunner extends Instrumentation {
 
+    // constants for supported instrumentation arguments
     public static final String ARGUMENT_TEST_CLASS = "class";
-
     private static final String ARGUMENT_TEST_SIZE = "size";
     private static final String ARGUMENT_LOG_ONLY = "log";
     private static final String ARGUMENT_ANNOTATION = "annotation";
     private static final String ARGUMENT_NOT_ANNOTATION = "notAnnotation";
+    private static final String ARGUMENT_DELAY_MSEC = "delay_msec";
+    private static final String ARGUMENT_COVERAGE = "coverage";
+    private static final String ARGUMENT_COVERAGE_PATH = "coverageFile";
+    private static final String ARGUMENT_SUITE_ASSIGNMENT = "suiteAssignment";
+    private static final String ARGUMENT_DEBUG = "debug";
+    // TODO: consider supporting 'count' from InstrumentationTestRunner
 
-    /**
-     * The following keys are used in the status bundle to provide structured reports to
-     * an IInstrumentationWatcher.
-     */
+    private static final String LOG_TAG = "AndroidJUnitRunner";
 
-    /**
-     * This value, if stored with key {@link android.app.Instrumentation#REPORT_KEY_IDENTIFIER},
-     * identifies InstrumentationTestRunner as the source of the report.  This is sent with all
-     * status messages.
-     */
-    public static final String REPORT_VALUE_ID = "InstrumentationTestRunner";
-    /**
-     * If included in the status or final bundle sent to an IInstrumentationWatcher, this key
-     * identifies the total number of tests that are being run.  This is sent with all status
-     * messages.
-     */
-    public static final String REPORT_KEY_NUM_TOTAL = "numtests";
-    /**
-     * If included in the status or final bundle sent to an IInstrumentationWatcher, this key
-     * identifies the sequence number of the current test.  This is sent with any status message
-     * describing a specific test being started or completed.
-     */
-    public static final String REPORT_KEY_NUM_CURRENT = "current";
-    /**
-     * If included in the status or final bundle sent to an IInstrumentationWatcher, this key
-     * identifies the name of the current test class.  This is sent with any status message
-     * describing a specific test being started or completed.
-     */
-    public static final String REPORT_KEY_NAME_CLASS = "class";
-    /**
-     * If included in the status or final bundle sent to an IInstrumentationWatcher, this key
-     * identifies the name of the current test.  This is sent with any status message
-     * describing a specific test being started or completed.
-     */
-    public static final String REPORT_KEY_NAME_TEST = "test";
-
-    /**
-     * The test is starting.
-     */
-    public static final int REPORT_VALUE_RESULT_START = 1;
-    /**
-     * The test completed successfully.
-     */
-    public static final int REPORT_VALUE_RESULT_OK = 0;
-    /**
-     * The test completed with an error.
-     */
-    public static final int REPORT_VALUE_RESULT_ERROR = -1;
-    /**
-     * The test completed with a failure.
-     */
-    public static final int REPORT_VALUE_RESULT_FAILURE = -2;
-    /**
-     * The test was ignored.
-     */
-    public static final int REPORT_VALUE_RESULT_IGNORED = -3;
-    /**
-     * If included in the status bundle sent to an IInstrumentationWatcher, this key
-     * identifies a stack trace describing an error or failure.  This is sent with any status
-     * message describing a specific test being completed.
-     */
-    public static final String REPORT_KEY_STACK = "stack";
-
-    private static final String LOG_TAG = "InstrumentationTestRunner";
-
-    private final Bundle mResults = new Bundle();
     private Bundle mArguments;
 
     @Override
@@ -200,8 +157,17 @@
         return mArguments;
     }
 
-    private boolean getBooleanArgument(Bundle arguments, String tag) {
-        String tagString = arguments.getString(tag);
+    /**
+     * Set the arguments.
+     *
+     * @VisibleForTesting
+     */
+    void setArguments(Bundle args) {
+        mArguments = args;
+    }
+
+    private boolean getBooleanArgument(String tag) {
+        String tagString = getArguments().getString(tag);
         return tagString != null && Boolean.parseBoolean(tagString);
     }
 
@@ -218,17 +184,17 @@
     public void onStart() {
         prepareLooper();
 
-        if (getBooleanArgument(getArguments(), "debug")) {
+        if (getBooleanArgument(ARGUMENT_DEBUG)) {
             Debug.waitForDebugger();
         }
 
         ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
         PrintStream writer = new PrintStream(byteArrayOutputStream);
+        List<RunListener> listeners = new ArrayList<RunListener>();
+
         try {
             JUnitCore testRunner = new JUnitCore();
-            testRunner.addListener(new TextListener(writer));
-            WatcherResultPrinter detailedResultPrinter = new WatcherResultPrinter();
-            testRunner.addListener(detailedResultPrinter);
+            addListeners(listeners, testRunner, writer);
 
             TestRequest testRequest = buildRequest(getArguments(), writer);
             Result result = testRunner.run(testRequest.getRequest());
@@ -243,15 +209,65 @@
             t.printStackTrace(writer);
 
         } finally {
+            Bundle results = new Bundle();
+            reportRunEnded(listeners, writer, results);
             writer.close();
-            mResults.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
+            results.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
                     String.format("\n%s",
                             byteArrayOutputStream.toString()));
-            finish(Activity.RESULT_OK, mResults);
+            finish(Activity.RESULT_OK, results);
         }
 
     }
 
+    private void addListeners(List<RunListener> listeners, JUnitCore testRunner,
+            PrintStream writer) {
+        if (getBooleanArgument(ARGUMENT_SUITE_ASSIGNMENT)) {
+            addListener(listeners, testRunner, new SuiteAssignmentPrinter(writer));
+        } else {
+            addListener(listeners, testRunner, new TextListener(writer));
+            addListener(listeners, testRunner, new InstrumentationResultPrinter(this));
+            addDelayListener(listeners, testRunner);
+            addCoverageListener(listeners, testRunner);
+        }
+    }
+
+    private void addListener(List<RunListener> list, JUnitCore testRunner, RunListener listener) {
+        list.add(listener);
+        testRunner.addListener(listener);
+    }
+
+    private void addCoverageListener(List<RunListener> list, JUnitCore testRunner) {
+        if (getBooleanArgument(ARGUMENT_COVERAGE)) {
+            String coverageFilePath = getArguments().getString(ARGUMENT_COVERAGE_PATH);
+            addListener(list, testRunner, new CoverageListener(this, coverageFilePath));
+        }
+    }
+
+    /**
+     * Sets up listener to inject {@link #ARGUMENT_DELAY_MSEC}, if specified.
+     * @param testRunner
+     */
+    private void addDelayListener(List<RunListener> list, JUnitCore testRunner) {
+        try {
+            Object delay = getArguments().get(ARGUMENT_DELAY_MSEC);  // Accept either string or int
+            if (delay != null) {
+                int delayMsec = Integer.parseInt(delay.toString());
+                addListener(list, testRunner, new DelayInjector(delayMsec));
+            }
+        } catch (NumberFormatException e) {
+            Log.e(LOG_TAG, "Invalid delay_msec parameter", e);
+        }
+    }
+
+    private void reportRunEnded(List<RunListener> listeners, PrintStream writer, Bundle results) {
+        for (RunListener listener : listeners) {
+            if (listener instanceof InstrumentationRunListener) {
+                ((InstrumentationRunListener)listener).instrumentationRunFinished(writer, results);
+            }
+        }
+    }
+
     /**
      * Builds a {@link TestRequest} based on given input arguments.
      * <p/>
@@ -286,8 +302,7 @@
             builder.addAnnotationExclusionFilter(notAnnotation);
         }
 
-        boolean logOnly = getBooleanArgument(arguments, ARGUMENT_LOG_ONLY);
-        if (logOnly) {
+        if (getBooleanArgument(ARGUMENT_LOG_ONLY)) {
             builder.setSkipExecution(true);
         }
         return builder.build(this);
@@ -320,91 +335,4 @@
             testRequestBuilder.addTestClass(testClassName);
         }
     }
-
-    /**
-     * This class sends status reports back to the IInstrumentationWatcher
-     */
-    private class WatcherResultPrinter extends RunListener {
-        private final Bundle mResultTemplate;
-        Bundle mTestResult;
-        int mTestNum = 0;
-        int mTestResultCode = 0;
-        String mTestClass = null;
-
-        public WatcherResultPrinter() {
-            mResultTemplate = new Bundle();
-        }
-
-        @Override
-        public void testRunStarted(Description description) throws Exception {
-            mResultTemplate.putString(Instrumentation.REPORT_KEY_IDENTIFIER, REPORT_VALUE_ID);
-            mResultTemplate.putInt(REPORT_KEY_NUM_TOTAL, description.testCount());
-        }
-
-        @Override
-        public void testRunFinished(Result result) throws Exception {
-            // TODO: implement this
-        }
-
-        /**
-         * send a status for the start of a each test, so long tests can be seen
-         * as "running"
-         */
-        @Override
-        public void testStarted(Description description) throws Exception {
-            String testClass = description.getClassName();
-            String testName = description.getMethodName();
-            mTestResult = new Bundle(mResultTemplate);
-            mTestResult.putString(REPORT_KEY_NAME_CLASS, testClass);
-            mTestResult.putString(REPORT_KEY_NAME_TEST, testName);
-            mTestResult.putInt(REPORT_KEY_NUM_CURRENT, ++mTestNum);
-            // pretty printing
-            if (testClass != null && !testClass.equals(mTestClass)) {
-                mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
-                        String.format("\n%s:", testClass));
-                mTestClass = testClass;
-            } else {
-                mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, "");
-            }
-
-            sendStatus(REPORT_VALUE_RESULT_START, mTestResult);
-            mTestResultCode = 0;
-        }
-
-        @Override
-        public void testFinished(Description description) throws Exception {
-            if (mTestResultCode == 0) {
-                mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, ".");
-            }
-            sendStatus(mTestResultCode, mTestResult);
-        }
-
-        @Override
-        public void testFailure(Failure failure) throws Exception {
-            mTestResultCode = REPORT_VALUE_RESULT_ERROR;
-            reportFailure(failure);
-        }
-
-
-        @Override
-        public void testAssumptionFailure(Failure failure) {
-            mTestResultCode = REPORT_VALUE_RESULT_FAILURE;
-            reportFailure(failure);
-        }
-
-        private void reportFailure(Failure failure) {
-            mTestResult.putString(REPORT_KEY_STACK, failure.getTrace());
-            // pretty printing
-            mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
-                    String.format("\nError in %s:\n%s",
-                            failure.getDescription().getDisplayName(), failure.getTrace()));
-        }
-
-        @Override
-        public void testIgnored(Description description) throws Exception {
-            testStarted(description);
-            mTestResultCode = REPORT_VALUE_RESULT_IGNORED;
-            testFinished(description);
-        }
-    }
 }
diff --git a/androidtestlib/src/com/android/test/runner/TestLoader.java b/androidtestlib/src/com/android/test/runner/TestLoader.java
index d5ad737..97c1083 100644
--- a/androidtestlib/src/com/android/test/runner/TestLoader.java
+++ b/androidtestlib/src/com/android/test/runner/TestLoader.java
@@ -23,6 +23,7 @@
 
 import java.io.PrintStream;
 import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
 import java.util.LinkedList;
 import java.util.List;
 
@@ -126,6 +127,11 @@
      * @return <code>true</code> if loadedClass is a test
      */
     private boolean isTestClass(Class<?> loadedClass) {
+        if (Modifier.isAbstract(loadedClass.getModifiers())) {
+            Log.v(LOG_TAG, String.format("Skipping abstract class %s: not a test",
+                    loadedClass.getName()));
+            return false;
+        }
         // TODO: try to find upstream junit calls to replace these checks
         if (junit.framework.Test.class.isAssignableFrom(loadedClass)) {
             return true;
diff --git a/androidtestlib/src/com/android/test/runner/TestRequestBuilder.java b/androidtestlib/src/com/android/test/runner/TestRequestBuilder.java
index 250a9fc..a15b70e 100644
--- a/androidtestlib/src/com/android/test/runner/TestRequestBuilder.java
+++ b/androidtestlib/src/com/android/test/runner/TestRequestBuilder.java
@@ -48,6 +48,10 @@
 
     private static final String LOG_TAG = "TestRequestBuilder";
 
+    public static final String LARGE_SIZE = "large";
+    public static final String MEDIUM_SIZE = "medium";
+    public static final String SMALL_SIZE = "small";
+
     private String[] mApkPaths;
     private TestLoader mTestLoader;
     private Filter mFilter = new AnnotationExclusionFilter(Suppress.class);
@@ -154,11 +158,11 @@
      * @param testSize
      */
     public void addTestSizeFilter(String testSize) {
-        if ("small".equals(testSize)) {
+        if (SMALL_SIZE.equals(testSize)) {
             mFilter = mFilter.intersect(new AnnotationInclusionFilter(SmallTest.class));
-        } else if ("medium".equals(testSize)) {
+        } else if (MEDIUM_SIZE.equals(testSize)) {
             mFilter = mFilter.intersect(new AnnotationInclusionFilter(MediumTest.class));
-        } else if ("large".equals(testSize)) {
+        } else if (LARGE_SIZE.equals(testSize)) {
             mFilter = mFilter.intersect(new AnnotationInclusionFilter(LargeTest.class));
         } else {
             Log.e(LOG_TAG, String.format("Unrecognized test size '%s'", testSize));
diff --git a/androidtestlib/src/com/android/test/runner/listener/CoverageListener.java b/androidtestlib/src/com/android/test/runner/listener/CoverageListener.java
new file mode 100644
index 0000000..4b36ea3
--- /dev/null
+++ b/androidtestlib/src/com/android/test/runner/listener/CoverageListener.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.test.runner.listener;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.io.File;
+import java.io.PrintStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * A test {@link RunListener} that generates EMMA code coverage.
+ */
+public class CoverageListener extends InstrumentationRunListener {
+
+    private String mCoverageFilePath;
+
+    /**
+     * If included in the status or final bundle sent to an IInstrumentationWatcher, this key
+     * identifies the path to the generated code coverage file.
+     */
+    private static final String REPORT_KEY_COVERAGE_PATH = "coverageFilePath";
+    // Default file name for code coverage
+    private static final String DEFAULT_COVERAGE_FILE_NAME = "coverage.ec";
+
+    private static final String LOG_TAG = null;
+
+    /**
+     * Creates a {@link CoverageListener).
+     *
+     * @param instr the {@link Instrumentation} that the test is running under
+     * @param customCoverageFilePath an optional user specified path for the coverage file
+     *         If null, file will be generated in test app's file directory.
+     */
+    public CoverageListener(Instrumentation instr, String customCoverageFilePath) {
+        super(instr);
+        mCoverageFilePath = customCoverageFilePath;
+        if (mCoverageFilePath == null) {
+            mCoverageFilePath = instr.getTargetContext().getFilesDir().getAbsolutePath() +
+                    File.separator + DEFAULT_COVERAGE_FILE_NAME;
+        }
+    }
+
+    @Override
+    public void instrumentationRunFinished(PrintStream writer, Bundle results) {
+        generateCoverageReport(writer, results);
+    }
+
+    private void generateCoverageReport(PrintStream writer, Bundle results) {
+        // use reflection to call emma dump coverage method, to avoid
+        // always statically compiling against emma jar
+        java.io.File coverageFile = new java.io.File(mCoverageFilePath);
+        try {
+            Class<?> emmaRTClass = Class.forName("com.vladium.emma.rt.RT");
+            Method dumpCoverageMethod = emmaRTClass.getMethod("dumpCoverageData",
+                    coverageFile.getClass(), boolean.class, boolean.class);
+
+            dumpCoverageMethod.invoke(null, coverageFile, false, false);
+
+            // output path to generated coverage file so it can be parsed by a test harness if
+            // needed
+            results.putString(REPORT_KEY_COVERAGE_PATH, mCoverageFilePath);
+            // also output a more user friendly msg
+            writer.format("\nGenerated code coverage data to %s",mCoverageFilePath);
+        } catch (ClassNotFoundException e) {
+            reportEmmaError(writer, "Is emma jar on classpath?", e);
+        } catch (SecurityException e) {
+            reportEmmaError(writer, e);
+        } catch (NoSuchMethodException e) {
+            reportEmmaError(writer, e);
+        } catch (IllegalArgumentException e) {
+            reportEmmaError(writer, e);
+        } catch (IllegalAccessException e) {
+            reportEmmaError(writer, e);
+        } catch (InvocationTargetException e) {
+            reportEmmaError(writer, e);
+        }
+    }
+
+    private void reportEmmaError(PrintStream writer, Exception e) {
+        reportEmmaError(writer, "", e);
+    }
+
+    private void reportEmmaError(PrintStream writer, String hint, Exception e) {
+        String msg = "Failed to generate emma coverage. " + hint;
+        Log.e(LOG_TAG, msg, e);
+        writer.format("\nError: %s", msg);
+    }
+}
diff --git a/androidtestlib/src/com/android/test/runner/listener/DelayInjector.java b/androidtestlib/src/com/android/test/runner/listener/DelayInjector.java
new file mode 100644
index 0000000..b092029
--- /dev/null
+++ b/androidtestlib/src/com/android/test/runner/listener/DelayInjector.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.test.runner.listener;
+
+import android.util.Log;
+
+import org.junit.runner.Description;
+import org.junit.runner.notification.RunListener;
+
+/**
+ * A {@link RunListener} that injects a given delay between tests.
+ */
+public class DelayInjector extends RunListener {
+
+    private final int mDelayMsec;
+
+    /**
+     * @param delayMsec
+     */
+    public DelayInjector(int delayMsec) {
+        mDelayMsec = delayMsec;
+    }
+
+    @Override
+    public void testRunStarted(Description description) throws Exception {
+        // delay before first test
+        delay();
+    }
+
+    @Override
+    public void testFinished(Description description) throws Exception {
+        // delay after every test
+        delay();
+    }
+
+    private void delay() {
+        try {
+            Thread.sleep(mDelayMsec);
+        } catch (InterruptedException e) {
+            Log.e("DelayInjector", "interrupted", e);
+        }
+    }
+}
diff --git a/androidtestlib/src/com/android/test/runner/listener/InstrumentationResultPrinter.java b/androidtestlib/src/com/android/test/runner/listener/InstrumentationResultPrinter.java
new file mode 100644
index 0000000..df392e2
--- /dev/null
+++ b/androidtestlib/src/com/android/test/runner/listener/InstrumentationResultPrinter.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.test.runner.listener;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+
+import org.junit.runner.Description;
+import org.junit.runner.Result;
+import org.junit.runner.notification.Failure;
+
+/**
+ * A {@link RunListener} that sends detailed pass/fail results back as instrumentation status
+ * bundles. This output appears when running the instrumentation in '-r' or raw mode.
+ */
+public class InstrumentationResultPrinter extends InstrumentationRunListener {
+
+    /**
+     * This value, if stored with key {@link android.app.Instrumentation#REPORT_KEY_IDENTIFIER},
+     * identifies AndroidJUnitRunner as the source of the report.  This is sent with all
+     * status messages.
+     */
+    public static final String REPORT_VALUE_ID = "AndroidJUnitRunner";
+    /**
+     * If included in the status or final bundle sent to an IInstrumentationWatcher, this key
+     * identifies the total number of tests that are being run.  This is sent with all status
+     * messages.
+     */
+    public static final String REPORT_KEY_NUM_TOTAL = "numtests";
+    /**
+     * If included in the status or final bundle sent to an IInstrumentationWatcher, this key
+     * identifies the sequence number of the current test.  This is sent with any status message
+     * describing a specific test being started or completed.
+     */
+    public static final String REPORT_KEY_NUM_CURRENT = "current";
+    /**
+     * If included in the status or final bundle sent to an IInstrumentationWatcher, this key
+     * identifies the name of the current test class.  This is sent with any status message
+     * describing a specific test being started or completed.
+     */
+    public static final String REPORT_KEY_NAME_CLASS = "class";
+    /**
+     * If included in the status or final bundle sent to an IInstrumentationWatcher, this key
+     * identifies the name of the current test.  This is sent with any status message
+     * describing a specific test being started or completed.
+     */
+    public static final String REPORT_KEY_NAME_TEST = "test";
+
+    /**
+     * The test is starting.
+     */
+    public static final int REPORT_VALUE_RESULT_START = 1;
+    /**
+     * The test completed successfully.
+     */
+    public static final int REPORT_VALUE_RESULT_OK = 0;
+    /**
+     * The test completed with an error.
+     */
+    public static final int REPORT_VALUE_RESULT_ERROR = -1;
+    /**
+     * The test completed with a failure.
+     */
+    public static final int REPORT_VALUE_RESULT_FAILURE = -2;
+    /**
+     * The test was ignored.
+     */
+    public static final int REPORT_VALUE_RESULT_IGNORED = -3;
+    /**
+     * If included in the status bundle sent to an IInstrumentationWatcher, this key
+     * identifies a stack trace describing an error or failure.  This is sent with any status
+     * message describing a specific test being completed.
+     */
+    public static final String REPORT_KEY_STACK = "stack";
+
+    private final Bundle mResultTemplate;
+    Bundle mTestResult;
+    int mTestNum = 0;
+    int mTestResultCode = 0;
+    String mTestClass = null;
+
+    public InstrumentationResultPrinter(Instrumentation i) {
+        super(i);
+        mResultTemplate = new Bundle();
+    }
+
+    @Override
+    public void testRunStarted(Description description) throws Exception {
+        mResultTemplate.putString(Instrumentation.REPORT_KEY_IDENTIFIER, REPORT_VALUE_ID);
+        mResultTemplate.putInt(REPORT_KEY_NUM_TOTAL, description.testCount());
+    }
+
+    @Override
+    public void testRunFinished(Result result) throws Exception {
+    }
+
+    /**
+     * send a status for the start of a each test, so long tests can be seen
+     * as "running"
+     */
+    @Override
+    public void testStarted(Description description) throws Exception {
+        String testClass = description.getClassName();
+        String testName = description.getMethodName();
+        mTestResult = new Bundle(mResultTemplate);
+        mTestResult.putString(REPORT_KEY_NAME_CLASS, testClass);
+        mTestResult.putString(REPORT_KEY_NAME_TEST, testName);
+        mTestResult.putInt(REPORT_KEY_NUM_CURRENT, ++mTestNum);
+        // pretty printing
+        if (testClass != null && !testClass.equals(mTestClass)) {
+            mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
+                    String.format("\n%s:", testClass));
+            mTestClass = testClass;
+        } else {
+            mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, "");
+        }
+
+        sendStatus(REPORT_VALUE_RESULT_START, mTestResult);
+        mTestResultCode = 0;
+    }
+
+    @Override
+    public void testFinished(Description description) throws Exception {
+        if (mTestResultCode == 0) {
+            mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, ".");
+        }
+        sendStatus(mTestResultCode, mTestResult);
+    }
+
+    @Override
+    public void testFailure(Failure failure) throws Exception {
+        mTestResultCode = REPORT_VALUE_RESULT_ERROR;
+        reportFailure(failure);
+    }
+
+
+    @Override
+    public void testAssumptionFailure(Failure failure) {
+        mTestResultCode = REPORT_VALUE_RESULT_FAILURE;
+        reportFailure(failure);
+    }
+
+    private void reportFailure(Failure failure) {
+        mTestResult.putString(REPORT_KEY_STACK, failure.getTrace());
+        // pretty printing
+        mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
+                String.format("\nError in %s:\n%s",
+                        failure.getDescription().getDisplayName(), failure.getTrace()));
+    }
+
+    @Override
+    public void testIgnored(Description description) throws Exception {
+        testStarted(description);
+        mTestResultCode = REPORT_VALUE_RESULT_IGNORED;
+        testFinished(description);
+    }
+}
diff --git a/androidtestlib/src/com/android/test/runner/listener/InstrumentationRunListener.java b/androidtestlib/src/com/android/test/runner/listener/InstrumentationRunListener.java
new file mode 100644
index 0000000..e9370af
--- /dev/null
+++ b/androidtestlib/src/com/android/test/runner/listener/InstrumentationRunListener.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.test.runner.listener;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+
+import org.junit.runner.notification.RunListener;
+
+import java.io.PrintStream;
+
+/**
+ * A {@link RunListener} that has access to a {@link Instrumentation}. This is useful for
+ * test result listeners that want to dump data back to the instrumentation results.
+ */
+public abstract class InstrumentationRunListener extends RunListener {
+
+    private final Instrumentation mInstr;
+
+    public InstrumentationRunListener(Instrumentation instr) {
+        mInstr = instr;
+    }
+
+    public Instrumentation getInstrumentation() {
+        return mInstr;
+    }
+
+    /**
+     * Convenience method for {@link #getInstrumentation()#sendStatus()}
+     */
+    public void sendStatus(int code, Bundle bundle) {
+        getInstrumentation().sendStatus(code, bundle);
+    }
+
+    /**
+     * Optional callback subclasses can implement. Will be called when instrumentation run
+     * completes.
+     *
+     * @param streamResult the {@link PrintStream} to instrumentation out.
+     * @param resultBundle the instrumentation result bundle. Can be used to inject key-value pairs
+     * into the instrumentation output when run in -r/raw mode
+     */
+    public void instrumentationRunFinished(PrintStream streamResult, Bundle resultBundle) {
+    }
+}
diff --git a/androidtestlib/src/com/android/test/runner/listener/SuiteAssignmentPrinter.java b/androidtestlib/src/com/android/test/runner/listener/SuiteAssignmentPrinter.java
new file mode 100644
index 0000000..ac4aa2c
--- /dev/null
+++ b/androidtestlib/src/com/android/test/runner/listener/SuiteAssignmentPrinter.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.test.runner.listener;
+
+import com.android.test.runner.TestRequestBuilder;
+
+import org.junit.runner.Description;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunListener;
+
+import java.io.PrintStream;
+
+/**
+ * This class measures the elapsed run time of each test, and used it to report back to the user
+ * which suite ({@link SmallSuite}, {@link MediumSuite}, {@link LargeSuite}) the test should belong
+ * to.
+ */
+public class SuiteAssignmentPrinter extends RunListener {
+    /**
+     * This constant defines the maximum allowed runtime (in ms) for a test included in the "small"
+     * suite. It is used to make an educated guess at what suite an unlabeled test belongs to.
+     */
+    private static final float SMALL_SUITE_MAX_RUNTIME = 100;
+
+    /**
+     * This constant defines the maximum allowed runtime (in ms) for a test included in the "medium"
+     * suite. It is used to make an educated guess at what suite an unlabeled test belongs to.
+     */
+    private static final float MEDIUM_SUITE_MAX_RUNTIME = 1000;
+
+    private final PrintStream mWriter;
+
+    public SuiteAssignmentPrinter(PrintStream writer) {
+        mWriter = writer;
+    }
+
+    private long mStartTime;
+    private boolean mTimingValid;
+
+    @Override
+    public void testStarted(Description description) throws Exception {
+        mTimingValid = true;
+        mStartTime = System.currentTimeMillis();
+    }
+
+    @Override
+    public void testFinished(Description description) throws Exception {
+        long runTime;
+        String assignmentSuite;
+        long endTime = System.currentTimeMillis();
+
+        if (!mTimingValid || mStartTime < 0) {
+            assignmentSuite = "NA";
+            runTime = -1;
+        } else {
+            runTime = endTime - mStartTime;
+            if (runTime < SMALL_SUITE_MAX_RUNTIME) {
+                assignmentSuite = TestRequestBuilder.SMALL_SIZE;
+            } else if (runTime < MEDIUM_SUITE_MAX_RUNTIME) {
+                assignmentSuite = TestRequestBuilder.MEDIUM_SIZE;
+            } else {
+                assignmentSuite = TestRequestBuilder.LARGE_SIZE;
+            }
+        }
+        // Clear mStartTime so that we can verify that it gets set next time.
+        mStartTime = -1;
+
+        mWriter.printf("%s#%s\n" + "in %s suite\n" + "runTime: %d ms\n",
+                        description.getClassName(), description.getMethodName(), assignmentSuite,
+                        runTime);
+    }
+
+    @Override
+    public void testFailure(Failure failure) throws Exception {
+        mTimingValid = false;
+    }
+
+    @Override
+    public void testAssumptionFailure(Failure failure) {
+        mTimingValid = false;
+    }
+
+    @Override
+    public void testIgnored(Description description) throws Exception {
+        mTimingValid = false;
+    }
+}
diff --git a/androidtestlib/tests/AndroidManifest.xml b/androidtestlib/tests/AndroidManifest.xml
index 36a84af..c8c181e 100644
--- a/androidtestlib/tests/AndroidManifest.xml
+++ b/androidtestlib/tests/AndroidManifest.xml
@@ -23,6 +23,7 @@
     <instrumentation android:name="com.android.test.runner.AndroidJUnitRunner"
                      android:targetPackage="com.android.testlib.tests"
                      android:label="Unit Tests for testlib."/>
+
     <uses-sdk android:minSdkVersion="8"/>
 
 </manifest>
diff --git a/androidtestlib/tests/src/com/android/test/MyAndroidTestCase.java b/androidtestlib/tests/src/com/android/test/MyAndroidTestCase.java
index 1e328ac..f48337e 100644
--- a/androidtestlib/tests/src/com/android/test/MyAndroidTestCase.java
+++ b/androidtestlib/tests/src/com/android/test/MyAndroidTestCase.java
@@ -17,6 +17,7 @@
 
 import android.content.Context;
 import android.test.AndroidTestCase;
+import android.util.Log;
 
 /**
  * Placeholder test to verify {@link Context} gets injected to {@link AndroidTestCase}.
@@ -24,4 +25,8 @@
 public class MyAndroidTestCase extends AndroidTestCase {
 
     // rely on testCaseSetupProperly to test for context
+
+    public MyAndroidTestCase() {
+        Log.i("MyAndroidTestCase", "I'm created");
+    }
 }
diff --git a/androidtestlib/tests/src/com/android/test/runner/AndroidJUnitRunnerTest.java b/androidtestlib/tests/src/com/android/test/runner/AndroidJUnitRunnerTest.java
index 010e23f..6a24a47 100644
--- a/androidtestlib/tests/src/com/android/test/runner/AndroidJUnitRunnerTest.java
+++ b/androidtestlib/tests/src/com/android/test/runner/AndroidJUnitRunnerTest.java
@@ -55,6 +55,7 @@
                 return mMockContext;
             }
         };
+        mAndroidJUnitRunner.setArguments(new Bundle());
         mStubStream = new PrintStream(new ByteArrayOutputStream());
         LittleMock.initMocks(this);
     }
diff --git a/androidtestlib/tests/src/com/android/test/runner/TestLoaderTest.java b/androidtestlib/tests/src/com/android/test/runner/TestLoaderTest.java
index 5ffff5c..34cadc1 100644
--- a/androidtestlib/tests/src/com/android/test/runner/TestLoaderTest.java
+++ b/androidtestlib/tests/src/com/android/test/runner/TestLoaderTest.java
@@ -34,6 +34,9 @@
     public static class JUnit3Test extends TestCase {
     }
 
+    public static abstract class AbstractTest extends TestCase {
+    }
+
     public static class JUnit4Test {
         @Test
         public void thisIsATest() {
@@ -93,4 +96,11 @@
         Assert.assertEquals(0, mLoader.getLoadFailures().size());
         Assert.assertTrue(mLoader.getLoadedClasses().contains(clazz));
     }
+
+    @Test
+    public void testLoadTests_abstract() {
+        Assert.assertNull(mLoader.loadIfTest(AbstractTest.class.getName()));
+        Assert.assertEquals(0, mLoader.getLoadedClasses().size());
+        Assert.assertEquals(0, mLoader.getLoadFailures().size());
+    }
 }