am 2fe8aed7: Merge "Add support for code coverage."

* commit '2fe8aed7542ee05ce504d69656475d1948e9c5b2':
  Add support for code coverage.
diff --git a/androidtestlib/src/com/android/test/runner/AndroidJUnitRunner.java b/androidtestlib/src/com/android/test/runner/AndroidJUnitRunner.java
index 21112df..90a3752 100644
--- a/androidtestlib/src/com/android/test/runner/AndroidJUnitRunner.java
+++ b/androidtestlib/src/com/android/test/runner/AndroidJUnitRunner.java
@@ -24,15 +24,20 @@
 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 org.junit.internal.TextListener;
 import org.junit.runner.JUnitCore;
 import org.junit.runner.Result;
+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,6 +109,15 @@
  * 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 {
 
@@ -114,10 +128,11 @@
     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 LOG_TAG = "AndroidJUnitRunner";
 
-    private final Bundle mResults = new Bundle();
     private Bundle mArguments;
 
     @Override
@@ -171,12 +186,15 @@
 
         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));
-            testRunner.addListener(new InstrumentationResultPrinter(this));
-            addDelayListener(testRunner);
+            addListener(listeners, testRunner, new TextListener(writer));
+            addListener(listeners, testRunner, new InstrumentationResultPrinter(this));
+            addDelayListener(listeners, testRunner);
+            addCoverageListener(listeners, testRunner);
 
             TestRequest testRequest = buildRequest(getArguments(), writer);
             Result result = testRunner.run(testRequest.getRequest());
@@ -191,15 +209,53 @@
             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 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/>
@@ -267,20 +323,4 @@
             testRequestBuilder.addTestClass(testClassName);
         }
     }
-
-    /**
-     * Sets up listener to inject {@link #ARGUMENT_DELAY_MSEC}, if specified.
-     * @param testRunner
-     */
-    private void addDelayListener(JUnitCore testRunner) {
-        try {
-            Object delay = getArguments().get(ARGUMENT_DELAY_MSEC);  // Accept either string or int
-            if (delay != null) {
-                int delayMsec = Integer.parseInt(delay.toString());
-                testRunner.addListener(new DelayInjector(delayMsec));
-            }
-        } catch (NumberFormatException e) {
-            Log.e(LOG_TAG, "Invalid delay_msec parameter", e);
-        }
-    }
 }
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/InstrumentationResultPrinter.java b/androidtestlib/src/com/android/test/runner/listener/InstrumentationResultPrinter.java
index cc38829..df392e2 100644
--- a/androidtestlib/src/com/android/test/runner/listener/InstrumentationResultPrinter.java
+++ b/androidtestlib/src/com/android/test/runner/listener/InstrumentationResultPrinter.java
@@ -105,7 +105,6 @@
 
     @Override
     public void testRunFinished(Result result) throws Exception {
-        // TODO: implement this
     }
 
     /**
diff --git a/androidtestlib/src/com/android/test/runner/listener/InstrumentationRunListener.java b/androidtestlib/src/com/android/test/runner/listener/InstrumentationRunListener.java
index f189cf4..e9370af 100644
--- a/androidtestlib/src/com/android/test/runner/listener/InstrumentationRunListener.java
+++ b/androidtestlib/src/com/android/test/runner/listener/InstrumentationRunListener.java
@@ -20,6 +20,8 @@
 
 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.
@@ -42,4 +44,15 @@
     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) {
+    }
 }