Add ability to parse test package results from xml.

Also fix xml generation for stack traces.

Bug 5171576

Change-Id: I3cbcdb69c6887bace2b1bd0610aeba7f1f8b8884
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/result/AbstractXmlPullParser.java b/tools/tradefed-host/src/com/android/cts/tradefed/result/AbstractXmlPullParser.java
new file mode 100644
index 0000000..c5f1e2d
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/result/AbstractXmlPullParser.java
@@ -0,0 +1,94 @@
+/*
+ * 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.result;
+
+import com.android.tradefed.util.xml.AbstractXmlParser.ParseException;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import java.io.IOException;
+import java.io.Reader;
+
+/**
+ * Helper abstract class for XmlPullParser
+ *
+ * TODO: move to com.android.tradefed.util.xml
+ */
+public abstract class AbstractXmlPullParser {
+
+    /**
+     * Parse the summary data from the given input data.
+     *
+     * @param xmlReader the input XML
+     * @throws ParseException if failed to parse the summary data.
+     */
+    public void parse(Reader xmlReader) throws ParseException {
+        try {
+            XmlPullParserFactory fact = org.xmlpull.v1.XmlPullParserFactory.newInstance();
+            XmlPullParser parser = fact.newPullParser();
+            parser.setInput (xmlReader);
+            parse(parser);
+        } catch (XmlPullParserException e) {
+           throw new ParseException(e);
+        } catch (IOException e) {
+            throw new ParseException(e);
+        }
+    }
+
+    abstract void parse(XmlPullParser parser) throws XmlPullParserException, IOException;
+
+    /**
+     * Parse an integer value from an XML attribute
+     *
+     * @param parser the {@link XmlPullParser}
+     * @param name the attribute name
+     * @return the parsed value or 0 if it could not be parsed
+     */
+    protected int parseIntAttr(XmlPullParser parser, String name) {
+        try {
+            String value = parser.getAttributeValue(null, name);
+            if (value != null) {
+                return Integer.parseInt(value);
+            }
+        } catch (NumberFormatException e) {
+            // ignore
+        }
+        return 0;
+    }
+
+    /**
+     * Parse a boolean attribute value
+     */
+    protected boolean parseBooleanAttr(XmlPullParser parser, String name) {
+        String stringValue = parser.getAttributeValue(null, name);
+        return stringValue != null &&
+                Boolean.parseBoolean(stringValue);
+    }
+
+    /**
+     * Helper method for retrieving attribute value with given name
+     *
+     * @param parser the XmlPullParser
+     * @param name the attribute name
+     * @return the attribute value
+     */
+    protected String getAttribute(XmlPullParser parser, String name) {
+        return parser.getAttributeValue(null, name);
+    }
+}
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/result/Test.java b/tools/tradefed-host/src/com/android/cts/tradefed/result/Test.java
index ab6cb85..184b8ab 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/result/Test.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/result/Test.java
@@ -20,13 +20,15 @@
 import com.android.tradefed.result.TestResult.TestStatus;
 
 import org.kxml2.io.KXmlSerializer;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
 
 /**
  * Data structure that represents a "Test" result XML element.
  */
-class Test {
+class Test extends AbstractXmlPullParser {
 
     static final String TAG = "Test";
     private static final String NAME_ATTR = "name";
@@ -35,6 +37,7 @@
     private static final String STARTTIME_ATTR = "starttime";
     private static final String RESULT_ATTR = "result";
     private static final String SCENE_TAG = "FailedScene";
+    private static final String STACK_TAG = "StackTrace";
 
     private String mName;
     private String mResult;
@@ -81,6 +84,26 @@
         return mName;
     }
 
+    public String getResult() {
+        return mResult;
+    }
+
+    public String getMessage() {
+        return mMessage;
+    }
+
+    public String getStartTime() {
+        return mStartTime;
+    }
+
+    public String getEndTime() {
+        return mEndTime;
+    }
+
+    public String getStackTrace() {
+        return mStackTrace;
+    }
+
     /**
      * Serialize this object and all its contents to XML.
      *
@@ -98,7 +121,11 @@
         if (mMessage != null) {
             serializer.startTag(CtsXmlResultReporter.ns, SCENE_TAG);
             serializer.attribute(CtsXmlResultReporter.ns, MESSAGE_ATTR, mMessage);
-            serializer.text(mStackTrace);
+            if (mStackTrace != null) {
+                serializer.startTag(CtsXmlResultReporter.ns, STACK_TAG);
+                serializer.text(mStackTrace);
+                serializer.endTag(CtsXmlResultReporter.ns, STACK_TAG);
+            }
             serializer.endTag(CtsXmlResultReporter.ns, SCENE_TAG);
         }
         serializer.endTag(CtsXmlResultReporter.ns, TAG);
@@ -146,4 +173,34 @@
         }
         return stack;
     }
+
+    /**
+     * Populates this class with test result data parsed from XML.
+     *
+     * @param parser the {@link XmlPullParser}. Expected to be pointing at start
+     *            of a Test tag
+     */
+    @Override
+    void parse(XmlPullParser parser) throws XmlPullParserException, IOException {
+        if (!parser.getName().equals(TAG)) {
+            throw new XmlPullParserException(String.format(
+                    "invalid XML: Expected %s tag but received %s", TAG, parser.getName()));
+        }
+        setName(getAttribute(parser, NAME_ATTR));
+        mResult = getAttribute(parser, RESULT_ATTR);
+        mStartTime = getAttribute(parser, STARTTIME_ATTR);
+        mEndTime = getAttribute(parser, ENDTIME_ATTR);
+
+        int eventType = parser.next();
+        while (eventType != XmlPullParser.END_DOCUMENT) {
+            if (eventType == XmlPullParser.START_TAG && parser.getName().equals(SCENE_TAG)) {
+                mMessage = getAttribute(parser, MESSAGE_ATTR);
+            } else if (eventType == XmlPullParser.START_TAG && parser.getName().equals(STACK_TAG)) {
+                mStackTrace = parser.nextText();
+            } else if (eventType == XmlPullParser.END_TAG && parser.getName().equals(TAG)) {
+                return;
+            }
+            eventType = parser.next();
+        }
+    }
 }
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 43db56e..13b793b 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
@@ -18,15 +18,18 @@
 import com.android.tradefed.result.TestResult;
 
 import org.kxml2.io.KXmlSerializer;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
+import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.Map;
 
 /**
  * Data structure that represents a "TestCase" XML element and its children.
  */
-class TestCase {
+class TestCase extends AbstractXmlPullParser {
 
     static final String TAG = "TestCase";
 
@@ -54,6 +57,13 @@
     }
 
     /**
+     * Gets the child tests
+     */
+    public Collection<Test> getTests() {
+        return mChildTestMap.values();
+    }
+
+    /**
      * Inserts given test result
      *
      * @param testName
@@ -90,4 +100,30 @@
         }
        serializer.endTag(CtsXmlResultReporter.ns, TAG);
     }
+
+    /**
+     * Populates this class with test case result data parsed from XML.
+     *
+     * @param parser the {@link XmlPullParser}. Expected to be pointing at start
+     *            of a TestCase tag
+     */
+    @Override
+    void parse(XmlPullParser parser) throws XmlPullParserException, IOException {
+        if (!parser.getName().equals(TAG)) {
+            throw new XmlPullParserException(String.format(
+                    "invalid XML: Expected %s tag but received %s", TAG, parser.getName()));
+        }
+        setName(getAttribute(parser, "name"));
+        int eventType = parser.next();
+        while (eventType != XmlPullParser.END_DOCUMENT) {
+            if (eventType == XmlPullParser.START_TAG && parser.getName().equals(Test.TAG)) {
+                Test test = new Test();
+                test.parse(parser);
+                insertTest(test);
+            } else if (eventType == XmlPullParser.END_TAG && parser.getName().equals(TAG)) {
+                return;
+            }
+            eventType = parser.next();
+        }
+    }
 }
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/result/TestPackageResult.java b/tools/tradefed-host/src/com/android/cts/tradefed/result/TestPackageResult.java
index c2d7bd9..bf9ba31 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/result/TestPackageResult.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/result/TestPackageResult.java
@@ -20,8 +20,11 @@
 import com.android.tradefed.result.TestResult;
 
 import org.kxml2.io.KXmlSerializer;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
@@ -31,7 +34,7 @@
  * <p/>
  * Provides methods to serialize to XML.
  */
-class TestPackageResult  {
+class TestPackageResult  extends AbstractXmlPullParser {
 
     static final String TAG = "TestPackage";
     private static final String DIGEST_ATTR = "digest";
@@ -71,6 +74,13 @@
     }
 
     /**
+     * Return the {@link TestSuite}s
+     */
+    public Collection<TestSuite> getTestSuites() {
+        return mSuiteRoot.getTestSuites();
+    }
+
+    /**
      * Adds a test result to this test package
      *
      * @param testId
@@ -106,4 +116,33 @@
         mSuiteRoot.serialize(serializer);
         serializer.endTag(ns, TAG);
     }
+
+    /**
+     * Populates this class with package result data parsed from XML.
+     *
+     * @param parser the {@link XmlPullParser}. Expected to be pointing at start
+     *            of TestPackage tag
+     */
+    @Override
+    void parse(XmlPullParser parser) throws XmlPullParserException, IOException {
+        if (!parser.getName().equals(TAG)) {
+            throw new XmlPullParserException(String.format(
+                    "invalid XML: Expected %s tag but received %s", TAG, parser.getName()));
+        }
+        setAppPackageName(getAttribute(parser, APP_PACKAGE_NAME_ATTR));
+        setName(getAttribute(parser, NAME_ATTR));
+        setDigest(getAttribute(parser, DIGEST_ATTR));
+        int eventType = parser.getEventType();
+        while (eventType != XmlPullParser.END_DOCUMENT) {
+            if (eventType == XmlPullParser.START_TAG && parser.getName().equals(TestSuite.TAG)) {
+                TestSuite suite = new TestSuite();
+                suite.parse(parser);
+                mSuiteRoot.insertSuite(suite);
+            }
+            if (eventType == XmlPullParser.END_TAG && parser.getName().equals(TAG)) {
+                return;
+            }
+            eventType = parser.next();
+        }
+    }
 }
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/result/TestResults.java b/tools/tradefed-host/src/com/android/cts/tradefed/result/TestResults.java
new file mode 100644
index 0000000..6900e58
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/result/TestResults.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2010 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.result;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Data structure for the detailed CTS test results.
+ * <p/>
+ * Can deserialize results for test packages from XML
+ */
+class TestResults extends AbstractXmlPullParser {
+
+    private List<TestPackageResult> mPackages = new ArrayList<TestPackageResult>();
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    void parse(XmlPullParser parser) throws XmlPullParserException, IOException {
+        int eventType = parser.getEventType();
+        while (eventType != XmlPullParser.END_DOCUMENT) {
+            if (eventType == XmlPullParser.START_TAG && parser.getName().equals(
+                    TestPackageResult.TAG)) {
+                TestPackageResult pkg = new TestPackageResult();
+                pkg.parse(parser);
+                mPackages.add(pkg);
+            }
+            eventType = parser.next();
+        }
+    }
+
+    /**
+     * @return the list of parsed {@link TestPackageResult}.
+     */
+    public List<TestPackageResult> getPackages() {
+        return mPackages;
+    }
+}
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/result/TestSuite.java b/tools/tradefed-host/src/com/android/cts/tradefed/result/TestSuite.java
index f7a44fc..2de1d13 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/result/TestSuite.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/result/TestSuite.java
@@ -18,8 +18,11 @@
 import com.android.tradefed.result.TestResult;
 
 import org.kxml2.io.KXmlSerializer;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
+import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -27,7 +30,7 @@
 /**
  * Data structure that represents a "TestSuite" XML element and its children.
  */
-class TestSuite {
+class TestSuite extends AbstractXmlPullParser {
 
     static final String TAG = "TestSuite";
 
@@ -83,6 +86,20 @@
     }
 
     /**
+     * Gets all the child {@link TestSuite}s
+     */
+    public Collection<TestSuite> getTestSuites() {
+        return mChildSuiteMap.values();
+    }
+
+    /**
+     * Gets all the child {@link TestCase}s
+     */
+    public Collection<TestCase> getTestCases() {
+        return mChildTestCaseMap.values();
+    }
+
+    /**
      * Get the child {@link TestSuite} with given name, creating if necessary.
      *
      * @param suiteName
@@ -132,4 +149,49 @@
             serializer.endTag(CtsXmlResultReporter.ns, TAG);
         }
     }
+
+    /**
+     * Populates this class with suite result data parsed from XML.
+     *
+     * @param parser the {@link XmlPullParser}. Expected to be pointing at start
+     *            of a TestSuite tag
+     */
+    @Override
+    void parse(XmlPullParser parser) throws XmlPullParserException, IOException {
+        if (!parser.getName().equals(TAG)) {
+            throw new XmlPullParserException(String.format(
+                    "invalid XML: Expected %s tag but received %s", TAG, parser.getName()));
+        }
+        setName(getAttribute(parser, "name"));
+        int eventType = parser.next();
+        while (eventType != XmlPullParser.END_DOCUMENT) {
+            if (eventType == XmlPullParser.START_TAG && parser.getName().equals(TestSuite.TAG)) {
+                TestSuite suite = new TestSuite();
+                suite.parse(parser);
+                insertSuite(suite);
+            } else if (eventType == XmlPullParser.START_TAG && parser.getName().equals(
+                    TestCase.TAG)) {
+                TestCase testCase = new TestCase();
+                testCase.parse(parser);
+                insertTestCase(testCase);
+            } else if (eventType == XmlPullParser.END_TAG && parser.getName().equals(TAG)) {
+                return;
+            }
+            eventType = parser.next();
+        }
+    }
+
+    /**
+     * Adds a child {@link TestCase}.
+     */
+    public void insertTestCase(TestCase testCase) {
+        mChildTestCaseMap.put(testCase.getName(), testCase);
+    }
+
+    /**
+     * Adds a child {@link TestSuite}.
+     */
+    public void insertSuite(TestSuite suite) {
+        mChildSuiteMap.put(suite.getName(), 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 1897387..4e6b914 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
@@ -141,7 +141,7 @@
         assertTrue(output.contains(
                 "<Summary failed=\"1\" notExecuted=\"0\" timeout=\"0\" pass=\"0\" />"));
         final String failureTag =
-                "<FailedScene message=\"this is a trace\">this is a tracemore trace";
+                "<FailedScene message=\"this is a trace\">      <StackTrace>this is a tracemore trace";
         assertTrue(output.contains(failureTag));
     }
 
diff --git a/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/TestResultsTest.java b/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/TestResultsTest.java
new file mode 100644
index 0000000..a0360cd
--- /dev/null
+++ b/tools/tradefed-host/tests/src/com/android/cts/tradefed/result/TestResultsTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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.result;
+
+import com.android.tradefed.util.xml.AbstractXmlParser.ParseException;
+
+import java.io.StringReader;
+
+/**
+ * Unit tests for {@link TestResults} parsing.
+ */
+public class TestResultsTest extends junit.framework.TestCase {
+
+    private static final String RESULT_START = "<TestResult>";
+    private static final String RESULT_END = "</TestResult>";
+    private static final String TEST_PACKAGE_START =
+        "<TestPackage name=\"pkgName\" appPackageName=\"appPkgName\" digest=\"digValue\" >";
+    private static final String TEST_PACKAGE_END = "</TestPackage>";
+
+    private static final String TEST_PACKAGE_FULL =
+        RESULT_START +TEST_PACKAGE_START + TEST_PACKAGE_END + RESULT_END;
+
+    private static final String TEST_FULL =
+        RESULT_START + TEST_PACKAGE_START +
+        "<TestSuite name=\"com\" >" +
+            "<TestSuite name=\"example\" >" +
+                "<TestCase name=\"ExampleTest\" >" +
+                     "<Test name=\"testExample\"  endtime=\"et\" starttime=\"st\" result=\"fail\" >" +
+                         "<FailedScene message=\"msg\" >" +
+                             "<StackTrace>at ExampleTest.testExample()" +
+                             "</StackTrace>" +
+                         "</FailedScene>" +
+                      "</Test>" +
+                "</TestCase>" +
+            "</TestSuite>" +
+        "</TestSuite>";
+
+    /**
+     * Test parsing data with no result content
+     */
+    public void testParse_empty() throws Exception {
+        TestResults parser = new TestResults();
+        parser.parse(new StringReader("<Empty/>"));
+        assertEquals(0, parser.getPackages().size());
+    }
+
+    /**
+     * Test parsing data with a single test package
+     */
+    public void testParse_package() throws Exception {
+        TestResults parser = new TestResults();
+        parser.parse(new StringReader(TEST_PACKAGE_FULL));
+        assertEquals(1, parser.getPackages().size());
+        TestPackageResult pkg = parser.getPackages().get(0);
+        assertEquals("pkgName", pkg.getName());
+        assertEquals("appPkgName", pkg.getAppPackageName());
+        assertEquals("digValue", pkg.getDigest());
+    }
+
+    /**
+     * Test parsing not well formed XML data
+     */
+    public void testParse_corrupt() throws Exception {
+        TestResults parser = new TestResults();
+        // missing TEST_PACKAGE_END
+        try {
+            parser.parse(new StringReader(RESULT_START + TEST_PACKAGE_START + RESULT_END));
+            fail("ParseException not thrown");
+        } catch (ParseException e) {
+            // expected
+        }
+    }
+
+    /**
+     * Test parsing a result with a single failed test
+     */
+    public void testParse_test() throws Exception {
+        TestResults parser = new TestResults();
+        parser.parse(new StringReader(TEST_FULL));
+        assertEquals(1, parser.getPackages().size());
+        TestPackageResult pkg = parser.getPackages().get(0);
+        TestSuite comSuite = pkg.getTestSuites().iterator().next();
+        assertEquals("com", comSuite.getName());
+        TestSuite exampleSuite = comSuite.getTestSuites().iterator().next();
+        assertEquals("example", exampleSuite.getName());
+        TestCase exampleCase = exampleSuite.getTestCases().iterator().next();
+        assertEquals("ExampleTest", exampleCase.getName());
+        Test exampleTest = exampleCase.getTests().iterator().next();
+        assertEquals("testExample", exampleTest.getName());
+        assertEquals("msg", exampleTest.getMessage());
+        assertEquals("at ExampleTest.testExample()", exampleTest.getStackTrace());
+    }
+}