Round 2 of cts-tradefed XML tweaks

 - add deviceID
 - add TestPackage name and digest
 - remove erroneous extra TestCase result
 - default testPlan to 'NA' instead of 'unknown'

Change-Id: I20acde9654ba61a488b17248713d3bb592692c70
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/result/CtsXmlResultReporter.java b/tools/tradefed-host/src/com/android/cts/tradefed/result/CtsXmlResultReporter.java
index 14ed359..d6225d0 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/result/CtsXmlResultReporter.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/result/CtsXmlResultReporter.java
@@ -76,7 +76,7 @@
 
     // listen in on the plan option provided to CtsTest
     @Option(name = CtsTest.PLAN_OPTION, description = "the test plan to run.")
-    private String mPlanName = "unknown";
+    private String mPlanName = "NA";
 
     protected IBuildInfo mBuildInfo;
 
@@ -273,6 +273,7 @@
         for (Map.Entry<String, String> metricEntry : metricsCopy.entrySet()) {
             serializer.attribute(ns, metricEntry.getKey(), metricEntry.getValue());
         }
+        serializer.attribute(ns, "deviceID", getBuildInfo().getDeviceSerial());
         serializer.endTag(ns, "BuildInfo");
 
         serializeFeatureInfo(serializer, featureData);
@@ -445,9 +446,9 @@
             return;
         }
         serializer.startTag(ns, "TestPackage");
-        serializer.attribute(ns, "name", runResult.getName());
+        serializer.attribute(ns, "name", getMetric(runResult, CtsTest.PACKAGE_NAME_METRIC));
         serializer.attribute(ns, "appPackageName", runResult.getName());
-        serializer.attribute(ns, "digest", getMetric(runResult, "digest"));
+        serializer.attribute(ns, "digest", getMetric(runResult, CtsTest.PACKAGE_DIGEST_METRIC));
 
         // Dump the results.
 
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/result/TestCase.java b/tools/tradefed-host/src/com/android/cts/tradefed/result/TestCase.java
index 5d149c8..65e00ff 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/result/TestCase.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/result/TestCase.java
@@ -77,7 +77,6 @@
                 result.getStartTime()));
         serializer.attribute(CtsXmlResultReporter.ns, "endtime", TimeUtil.getTimestamp(
                 result.getEndTime()));
-        serializer.attribute(CtsXmlResultReporter.ns, "result", convertStatus(result.getStatus()));
 
         if (result.getStackTrace() != null) {
             String sanitizedStack = sanitizeStackTrace(result.getStackTrace());
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/testtype/CtsTest.java b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/CtsTest.java
index b42d7f0..cc5dd37 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/testtype/CtsTest.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/CtsTest.java
@@ -43,9 +43,11 @@
 import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.Queue;
 import java.util.Set;
 
@@ -57,7 +59,6 @@
  * Supports running all the tests contained in a CTS plan, or individual test packages.
  */
 public class CtsTest implements IDeviceTest, IResumableTest, IShardableTest, IBuildReceiver {
-
     private static final String LOG_TAG = "CtsTest";
 
     public static final String PLAN_OPTION = "plan";
@@ -65,13 +66,16 @@
     private static final String CLASS_OPTION = "class";
     private static final String METHOD_OPTION = "method";
 
+    public static final String PACKAGE_NAME_METRIC = "packageName";
+    public static final String PACKAGE_DIGEST_METRIC = "packageDigest";
+
     private ITestDevice mDevice;
 
     @Option(name = PLAN_OPTION, description = "the test plan to run.",
             importance = Importance.IF_UNSET)
     private String mPlanName = null;
 
-    @Option(name = PACKAGE_OPTION, description = "the test packages(s) to run.",
+    @Option(name = PACKAGE_OPTION, shortName = 'p', description = "the test packages(s) to run.",
             importance = Importance.IF_UNSET)
     private Collection<String> mPackageNames = new ArrayList<String>();
 
@@ -110,8 +114,11 @@
     private class KnownTests {
         private final IRemoteTest mTestForPackage;
         private final Collection<TestIdentifier> mKnownTests;
+        private final ITestPackageDef mPackageDef;
 
-        KnownTests(IRemoteTest testForPackage, Collection<TestIdentifier> knownTests) {
+        KnownTests(ITestPackageDef packageDef, IRemoteTest testForPackage,
+                Collection<TestIdentifier> knownTests) {
+            mPackageDef = packageDef;
             mTestForPackage = testForPackage;
             mKnownTests = knownTests;
         }
@@ -123,6 +130,10 @@
         Collection<TestIdentifier> getKnownTests() {
             return mKnownTests;
         }
+
+        ITestPackageDef getPackageDef() {
+            return mPackageDef;
+        }
     }
 
     /** list of remaining tests to execute */
@@ -245,9 +256,9 @@
         collectDeviceInfo(getDevice(), mCtsBuild, listener);
 
         while (!mRemainingTests.isEmpty()) {
-            KnownTests testPair = mRemainingTests.get(0);
+            KnownTests knownTests = mRemainingTests.get(0);
 
-            IRemoteTest test = testPair.getTestForPackage();
+            IRemoteTest test = knownTests.getTestForPackage();
             if (test instanceof IDeviceTest) {
                 ((IDeviceTest)test).setDevice(getDevice());
             }
@@ -255,8 +266,9 @@
                 ((IBuildReceiver)test).setBuild(mBuildInfo);
             }
 
-            ResultFilter filter = new ResultFilter(listener, testPair.getKnownTests());
+            ResultFilter filter = new ResultFilter(listener, knownTests.getKnownTests());
             test.run(filter);
+            forwardPackageDetails(knownTests.getPackageDef(), listener);
             mRemainingTests.remove(0);
         }
 
@@ -310,7 +322,7 @@
                     mClassName, mMethodName);
             if (testForPackage != null) {
                 Collection<TestIdentifier> knownTests = testPackage.getTests();
-                testList.add(new KnownTests(testForPackage, knownTests));
+                testList.add(new KnownTests(testPackage, testForPackage, knownTests));
             }
         } else {
             Log.e(LOG_TAG, String.format("Could not find test package uri %s", testUri));
@@ -471,4 +483,17 @@
         }
         return currentVal;
     }
+
+    /**
+     * Forward the digest and package name to the listener as a metric
+     *
+     * @param listener
+     */
+    private void forwardPackageDetails(ITestPackageDef def, ITestInvocationListener listener) {
+        Map<String, String> metrics = new HashMap<String, String>(2);
+        metrics.put(PACKAGE_NAME_METRIC, def.getName());
+        metrics.put(PACKAGE_DIGEST_METRIC, def.getDigest());
+        listener.testRunStarted(def.getUri(), 0);
+        listener.testRunEnded(0, metrics);
+    }
 }
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/testtype/ITestPackageDef.java b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/ITestPackageDef.java
index f4258d1..0348660 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/testtype/ITestPackageDef.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/ITestPackageDef.java
@@ -30,7 +30,7 @@
 public interface ITestPackageDef {
 
     /**
-     * Get the unique URI of the test package.
+     * Get the unique URI, aka the appPackageName, of the test package.
      * @return the {@link String} uri
      */
     public String getUri();
@@ -69,4 +69,19 @@
      */
     public Collection<TestIdentifier> getTests();
 
+    /**
+     * Return the sha1sum of the binary file for this test package.
+     * <p/>
+     * Will only return a valid value after {@link #createTest(File, String, String)} has been
+     * called.
+     *
+     * @return the sha1sum in {@link String} form
+     */
+    public String getDigest();
+
+    /**
+     * @return the name of this test package.
+     */
+    public String getName();
+
 }
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/testtype/TestPackageDef.java b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/TestPackageDef.java
index 20396f7..58f2c00 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/testtype/TestPackageDef.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/testtype/TestPackageDef.java
@@ -17,10 +17,20 @@
 
 import com.android.ddmlib.Log;
 import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.testtype.IRemoteTest;
 import com.android.tradefed.testtype.InstrumentationTest;
+import com.android.tradefed.util.StreamUtil;
 
+import java.io.BufferedInputStream;
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.DigestInputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.LinkedHashSet;
@@ -48,6 +58,7 @@
     private String mPackageToTest = null;
     private String mApkToTestName = null;
     private String mTestPackageName = null;
+    private String mDigest = null;
 
     // use a LinkedHashSet for predictable iteration insertion-order, and fast lookups
     private Collection<TestIdentifier> mTests = new LinkedHashSet<TestIdentifier>();
@@ -77,7 +88,11 @@
         mName = name;
     }
 
-    String getName() {
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getName() {
         return mName;
     }
 
@@ -149,16 +164,18 @@
         if (mIsHostSideTest) {
             Log.d(LOG_TAG, String.format("Creating host test for %s", mName));
             JarHostTest hostTest = new JarHostTest();
-            hostTest.setRunName(mName);
+            hostTest.setRunName(getUri());
             hostTest.setJarFileName(mJarPath);
             hostTest.setTests(filterTests(mTests, className, methodName));
+            mDigest = generateDigest(testCaseDir, mJarPath);
             return hostTest;
         } else if (mIsVMHostTest) {
             Log.d(LOG_TAG, String.format("Creating vm host test for %s", mName));
             VMHostTest vmHostTest = new VMHostTest();
-            vmHostTest.setRunName(mName);
+            vmHostTest.setRunName(getUri());
             vmHostTest.setJarFileName(mJarPath);
             vmHostTest.setTests(filterTests(mTests, className, methodName));
+            mDigest = generateDigest(testCaseDir, mJarPath);
             return vmHostTest;
         } else if (mIsSignatureTest) {
             // TODO: hardcode the runner/class/method for now, since current package xml
@@ -174,19 +191,22 @@
             addTest(new TestIdentifier(SIGNATURE_TEST_CLASS, SIGNATURE_TEST_METHOD));
             // mName means 'apk file name' for instrumentation tests
             instrTest.addInstallApk(String.format("%s.apk", mName), mAppNameSpace);
+            mDigest = generateDigest(testCaseDir, String.format("%s.apk", mName));
             return instrTest;
         } else if (mIsReferenceAppTest) {
             // a reference app test is just a InstrumentationTest with one extra apk to install
             InstrumentationApkTest instrTest = new InstrumentationApkTest();
             instrTest.addInstallApk(String.format("%s.apk", mApkToTestName), mPackageToTest);
-            return setInstrumentationTest(className, methodName, instrTest);
+            return setInstrumentationTest(className, methodName, instrTest, testCaseDir);
         } else {
             Log.d(LOG_TAG, String.format("Creating instrumentation test for %s", mName));
             InstrumentationApkTest instrTest = new InstrumentationApkTest();
-            return setInstrumentationTest(className, methodName, instrTest);
+            return setInstrumentationTest(className, methodName, instrTest, testCaseDir);
         }
     }
 
+
+
     /**
      * Populates given {@link InstrumentationApkTest} with data from the package xml
      *
@@ -196,8 +216,9 @@
      * @param instrTest
      * @return the populated {@link InstrumentationTest} or <code>null</code>
      */
-    private InstrumentationApkTest setInstrumentationTest(String className,
-            String methodName, InstrumentationApkTest instrTest) {
+    private InstrumentationTest setInstrumentationTest(String className,
+            String methodName, InstrumentationApkTest instrTest, File testCaseDir) {
+        instrTest.setRunName(getUri());
         instrTest.setPackageName(mAppNameSpace);
         instrTest.setRunnerName(mRunner);
         instrTest.setTestPackageName(mTestPackageName);
@@ -205,6 +226,7 @@
         instrTest.setMethodName(methodName);
         // mName means 'apk file name' for instrumentation tests
         instrTest.addInstallApk(String.format("%s.apk", mName), mAppNameSpace);
+        mDigest = generateDigest(testCaseDir, String.format("%s.apk", mName));
         if (mTests.size() > 1000) {
             // TODO: hack, large test suites can take longer to collect tests, increase timeout
             instrTest.setCollectsTestsShellTimeout(10*60*1000);
@@ -264,4 +286,68 @@
     public Collection<TestIdentifier> getTests() {
         return mTests;
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getDigest() {
+        return mDigest;
+    }
+
+    /**
+     * Generate a sha1sum digest for a file.
+     * <p/>
+     * Exposed for unit testing.
+     *
+     * @param fileDir the directory of the file
+     * @param fileName the name of the file
+     * @return a hex {@link String} of the digest
+     */
+     String generateDigest(File fileDir, String fileName) {
+        final String algorithm = "SHA-1";
+        InputStream fileStream = null;
+        DigestInputStream d  = null;
+        try {
+            fileStream = getFileStream(fileDir, fileName);
+            MessageDigest md = MessageDigest.getInstance(algorithm);
+            d = new DigestInputStream(fileStream, md);
+            byte[] buffer = new byte[8196];
+            while (d.read(buffer) != -1);
+            return toHexString(md.digest());
+        } catch (NoSuchAlgorithmException e) {
+            return algorithm + " not found";
+        } catch (IOException e) {
+            CLog.e(e);
+        } finally {
+            StreamUtil.closeStream(d);
+            StreamUtil.closeStream(fileStream);
+        }
+        return "failed to generate digest";
+    }
+
+    /**
+     * Retrieve an input stream for given file
+     * <p/>
+     * Exposed so unit tests can mock.
+     */
+    InputStream getFileStream(File fileDir, String fileName) throws FileNotFoundException {
+        InputStream fileStream;
+        fileStream = new BufferedInputStream(new FileInputStream(new File(fileDir, fileName)));
+        return fileStream;
+    }
+
+    /**
+     * Convert the given byte array into a lowercase hex string.
+     *
+     * @param arr The array to convert.
+     * @return The hex encoded string.
+     */
+    private String toHexString(byte[] arr) {
+        StringBuffer buf = new StringBuffer(arr.length * 2);
+        for (byte b : arr) {
+            buf.append(String.format("%02x", b & 0xFF));
+        }
+        return buf.toString();
+    }
 }
diff --git a/tools/tradefed-host/tests/src/com/android/cts/tradefed/UnitTests.java b/tools/tradefed-host/tests/src/com/android/cts/tradefed/UnitTests.java
index 2ff00d3..badccad 100644
--- a/tools/tradefed-host/tests/src/com/android/cts/tradefed/UnitTests.java
+++ b/tools/tradefed-host/tests/src/com/android/cts/tradefed/UnitTests.java
@@ -17,9 +17,10 @@
 
 import com.android.cts.tradefed.result.CtsXmlResultReporterTest;
 import com.android.cts.tradefed.targetprep.CtsSetupTest;
-import com.android.cts.tradefed.testtype.JarHostTestTest;
 import com.android.cts.tradefed.testtype.CtsTestTest;
+import com.android.cts.tradefed.testtype.JarHostTestTest;
 import com.android.cts.tradefed.testtype.PlanXmlParserTest;
+import com.android.cts.tradefed.testtype.TestPackageDefTest;
 import com.android.cts.tradefed.testtype.TestPackageXmlParserTest;
 
 import junit.framework.Test;
@@ -41,6 +42,7 @@
         addTestSuite(CtsTestTest.class);
         addTestSuite(PlanXmlParserTest.class);
         addTestSuite(TestPackageXmlParserTest.class);
+        addTestSuite(TestPackageDefTest.class);
     }
 
     public static Test suite() {
diff --git a/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/CtsXmlResultReporterTest.java b/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/CtsXmlResultReporterTest.java
index 2ce3ad6..577ecb4 100644
--- a/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/CtsXmlResultReporterTest.java
+++ b/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/CtsXmlResultReporterTest.java
@@ -113,7 +113,7 @@
         // TODO: consider doing xml based compare
         assertTrue(output.contains(
                 "<Summary failed=\"0\" notExecuted=\"0\" timeout=\"0\" pass=\"1\" />"));
-        assertTrue(output.contains("<TestPackage name=\"run\" appPackageName=\"run\" digest=\"\">"));
+        assertTrue(output.contains("<TestPackage name=\"\" appPackageName=\"run\" digest=\"\">"));
         assertTrue(output.contains("<TestCase name=\"FooTest\" priority=\"\">"));
 
         final String testCaseTag = String.format(
diff --git a/tools/tradefed-host/tests/src/com/android/cts/tradefed/testtype/TestPackageDefTest.java b/tools/tradefed-host/tests/src/com/android/cts/tradefed/testtype/TestPackageDefTest.java
new file mode 100644
index 0000000..2a5777b
--- /dev/null
+++ b/tools/tradefed-host/tests/src/com/android/cts/tradefed/testtype/TestPackageDefTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2011 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.cts.tradefed.testtype;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.InputStream;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for {@link TestPackageDef}.
+ */
+public class TestPackageDefTest extends TestCase {
+
+    /**
+     * Regression test for {@link TestPackageDef#generateDigest(File, String)} that ensures expected
+     * digest is generated for fixed data.
+     */
+    public void testGenerateDigest() {
+        TestPackageDef def = new TestPackageDef() {
+          @Override
+          InputStream getFileStream(File dir, String fileName) {
+              return new ByteArrayInputStream("test data for digest".getBytes());
+          }
+        };
+        String digest = def.generateDigest(new File("unused"), "alsounused");
+        assertNotNull(digest);
+        assertEquals("58c222b5f5f81b4b58891ec59924b9b2f530452e", digest);
+
+    }
+
+}