CtsXmlResultReporter.java revision 82d9daf8b1800ef1c7897f2cd3167b6b1920b9fa
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.cts.tradefed.result;
18
19import com.android.cts.tradefed.device.DeviceInfoCollector;
20import com.android.cts.tradefed.targetsetup.CtsBuildHelper;
21import com.android.ddmlib.Log;
22import com.android.ddmlib.Log.LogLevel;
23import com.android.ddmlib.testrunner.TestIdentifier;
24import com.android.tradefed.config.Option;
25import com.android.tradefed.result.CollectingTestListener;
26import com.android.tradefed.result.LogDataType;
27import com.android.tradefed.result.TestResult;
28import com.android.tradefed.result.TestRunResult;
29import com.android.tradefed.result.TestResult.TestStatus;
30import com.android.tradefed.targetsetup.IBuildInfo;
31import com.android.tradefed.targetsetup.IFolderBuildInfo;
32import com.android.tradefed.util.FileUtil;
33
34import org.kxml2.io.KXmlSerializer;
35
36import java.io.File;
37import java.io.FileNotFoundException;
38import java.io.FileOutputStream;
39import java.io.IOException;
40import java.io.InputStream;
41import java.io.OutputStream;
42import java.net.InetAddress;
43import java.net.UnknownHostException;
44import java.text.SimpleDateFormat;
45import java.util.Date;
46import java.util.HashMap;
47import java.util.LinkedHashMap;
48import java.util.Map;
49import java.util.concurrent.TimeUnit;
50
51/**
52 * Writes results to an XML files in the CTS format.
53 * <p/>
54 * Collects all test info in memory, then dumps to file when invocation is complete.
55 * <p/>
56 * Outputs xml in format governed by the cts_result.xsd
57 */
58public class CtsXmlResultReporter extends CollectingTestListener {
59
60    private static final String LOG_TAG = "CtsXmlResultReporter";
61
62    private static final String TEST_RESULT_FILE_NAME = "testResult.xml";
63    private static final String CTS_RESULT_FILE_VERSION = "2.0";
64    private static final String CTS_VERSION = "99";
65
66
67    private static final String[] CTS_RESULT_RESOURCES = {"cts_result.xsl", "cts_result.css",
68        "logo.gif", "newrule-green.png"};
69
70    /** the XML namespace */
71    private static final String ns = null;
72
73    private static final String REPORT_DIR_NAME = "output-file-path";
74    @Option(name=REPORT_DIR_NAME, description="root file system path to directory to store xml " +
75            "test results and associated logs. If not specified, results will be stored at " +
76            "<cts root>/repository/results")
77    protected File mReportDir = null;
78
79    protected IBuildInfo mBuildInfo;
80
81    private String mStartTime;
82
83    public void setReportDir(File reportDir) {
84        mReportDir = reportDir;
85    }
86
87    /**
88     * {@inheritDoc}
89     */
90    @Override
91    public void invocationStarted(IBuildInfo buildInfo) {
92        super.invocationStarted(buildInfo);
93        if (mReportDir == null) {
94            if (!(buildInfo instanceof IFolderBuildInfo)) {
95                throw new IllegalArgumentException("build info is not a IFolderBuildInfo");
96            }
97            IFolderBuildInfo ctsBuild = (IFolderBuildInfo)buildInfo;
98            try {
99                CtsBuildHelper buildHelper = new CtsBuildHelper(ctsBuild.getRootDir());
100                mReportDir = buildHelper.getResultsDir();
101
102            } catch (FileNotFoundException e) {
103                throw new IllegalArgumentException("unrecognized cts structure", e);
104            }
105        }
106        // create a unique directory for saving results, using old cts host convention
107        // TODO: in future, consider using LogFileSaver to create build-specific directories
108        mReportDir = new File(mReportDir, getResultTimestamp());
109        mReportDir.mkdirs();
110        mStartTime = getTimestamp();
111    }
112
113    /**
114     * {@inheritDoc}
115     */
116    @Override
117    public void testLog(String dataName, LogDataType dataType, InputStream dataStream) {
118        // TODO: implement this
119    }
120
121    /**
122     * {@inheritDoc}
123     */
124    @Override
125    public void testFailed(TestFailure status, TestIdentifier test, String trace) {
126        super.testFailed(status, test, trace);
127        Log.i(LOG_TAG, String.format("Test %s#%s: %s\n%s", test.getClassName(), test.getTestName(),
128                status.toString(), trace));
129    }
130
131    /**
132     * {@inheritDoc}
133     */
134    @Override
135    public void testRunEnded(long elapsedTime, Map<String, String> runMetrics) {
136        super.testRunEnded(elapsedTime, runMetrics);
137        Log.i(LOG_TAG, String.format("Test run %s complete. Tests passed %d, failed %d, error %d",
138                getCurrentRunResults().getName(), getCurrentRunResults().getNumPassedTests(),
139                getCurrentRunResults().getNumFailedTests(),
140                getCurrentRunResults().getNumErrorTests()));
141    }
142
143    /**
144     * {@inheritDoc}
145     */
146    @Override
147    public void invocationEnded(long elapsedTime) {
148        super.invocationEnded(elapsedTime);
149        createXmlResult(mReportDir, mStartTime, elapsedTime);
150        copyFormattingFiles(mReportDir);
151        zipResults(mReportDir);
152    }
153
154    /**
155     * Creates a report file and populates it with the report data from the completed tests.
156     */
157    private void createXmlResult(File reportDir, String startTimestamp, long elapsedTime) {
158        String endTime = getTimestamp();
159
160        OutputStream stream = null;
161        try {
162            stream = createOutputResultStream(reportDir);
163            KXmlSerializer serializer = new KXmlSerializer();
164            serializer.setOutput(stream, "UTF-8");
165            serializer.startDocument("UTF-8", false);
166            serializer.setFeature(
167                    "http://xmlpull.org/v1/doc/features.html#indent-output", true);
168            serializer.processingInstruction("xml-stylesheet type=\"text/xsl\"  " +
169                    "href=\"cts_result.xsl\"");
170            serializeResultsDoc(serializer, startTimestamp, endTime);
171            serializer.endDocument();
172            // TODO: output not executed timeout omitted counts
173            String msg = String.format("XML test result file generated at %s. Total tests %d, " +
174                    "Failed %d, Error %d", reportDir.getAbsolutePath(), getNumTotalTests(),
175                    getNumFailedTests(), getNumErrorTests());
176            Log.logAndDisplay(LogLevel.INFO, LOG_TAG, msg);
177            Log.logAndDisplay(LogLevel.INFO, LOG_TAG, String.format("Time: %s",
178                    formatElapsedTime(elapsedTime)));
179        } catch (IOException e) {
180            Log.e(LOG_TAG, "Failed to generate report data");
181        } finally {
182            if (stream != null) {
183                try {
184                    stream.close();
185                } catch (IOException ignored) {
186                }
187            }
188        }
189    }
190
191    /**
192     * Output the results XML.
193     *
194     * @param serializer the {@link KXmlSerializer} to use
195     * @param startTime the user-friendly starting time of the test invocation
196     * @param endTime the user-friendly ending time of the test invocation
197     * @throws IOException
198     */
199    private void serializeResultsDoc(KXmlSerializer serializer, String startTime, String endTime)
200            throws IOException {
201        serializer.startTag(ns, "TestResult");
202        // TODO: output test plan and profile values
203        serializer.attribute(ns, "testPlan", "unknown");
204        serializer.attribute(ns, "profile", "unknown");
205        serializer.attribute(ns, "starttime", startTime);
206        serializer.attribute(ns, "endtime", endTime);
207        serializer.attribute(ns, "version", CTS_RESULT_FILE_VERSION);
208
209        serializeDeviceInfo(serializer);
210        serializeHostInfo(serializer);
211        serializeTestSummary(serializer);
212        serializeTestResults(serializer);
213    }
214
215    /**
216     * Output the device info XML.
217     *
218     * @param serializer
219     */
220    private void serializeDeviceInfo(KXmlSerializer serializer) throws IOException {
221        serializer.startTag(ns, "DeviceInfo");
222
223        TestRunResult deviceInfoResult = findRunResult(DeviceInfoCollector.APP_PACKAGE_NAME);
224        if (deviceInfoResult == null) {
225            Log.w(LOG_TAG, String.format("Could not find device info run %s",
226                    DeviceInfoCollector.APP_PACKAGE_NAME));
227            return;
228        }
229        // Extract metrics that need extra handling, and then dump the remainder into BuildInfo
230        Map<String, String> metricsCopy = new HashMap<String, String>(
231                deviceInfoResult.getRunMetrics());
232        serializer.startTag(ns, "Screen");
233        String screenWidth = metricsCopy.remove(DeviceInfoCollector.SCREEN_WIDTH);
234        String screenHeight = metricsCopy.remove(DeviceInfoCollector.SCREEN_HEIGHT);
235        serializer.attribute(ns, "resolution", String.format("%sx%s", screenWidth, screenHeight));
236        serializer.endTag(ns, "Screen");
237
238        serializer.startTag(ns, "PhoneSubInfo");
239        serializer.attribute(ns, "subscriberId", metricsCopy.remove(
240                DeviceInfoCollector.PHONE_NUMBER));
241        serializer.endTag(ns, "PhoneSubInfo");
242
243        String featureData = metricsCopy.remove(DeviceInfoCollector.FEATURES);
244        String processData = metricsCopy.remove(DeviceInfoCollector.PROCESSES);
245
246        // dump the remaining metrics without translation
247        serializer.startTag(ns, "BuildInfo");
248        for (Map.Entry<String, String> metricEntry : metricsCopy.entrySet()) {
249            serializer.attribute(ns, metricEntry.getKey(), metricEntry.getValue());
250        }
251        serializer.endTag(ns, "BuildInfo");
252
253        serializeFeatureInfo(serializer, featureData);
254        serializeProcessInfo(serializer, processData);
255
256        serializer.endTag(ns, "DeviceInfo");
257    }
258
259    /**
260     * Prints XML indicating what features are supported by the device. It parses a string from the
261     * featureData argument that is in the form of "feature1:true;feature2:false;featuer3;true;"
262     * with a trailing semi-colon.
263     *
264     * <pre>
265     *  <FeatureInfo>
266     *     <Feature name="android.name.of.feature" available="true" />
267     *     ...
268     *   </FeatureInfo>
269     * </pre>
270     *
271     * @param serializer used to create XML
272     * @param featureData raw unparsed feature data
273     */
274    private void serializeFeatureInfo(KXmlSerializer serializer, String featureData) throws IOException {
275        serializer.startTag(ns, "FeatureInfo");
276
277        if (featureData == null) {
278            featureData = "";
279        }
280
281        String[] featurePairs = featureData.split(";");
282        for (String featurePair : featurePairs) {
283            String[] nameTypeAvailability = featurePair.split(":");
284            if (nameTypeAvailability.length >= 3) {
285                serializer.startTag(ns, "Feature");
286                serializer.attribute(ns, "name", nameTypeAvailability[0]);
287                serializer.attribute(ns, "type", nameTypeAvailability[1]);
288                serializer.attribute(ns, "available", nameTypeAvailability[2]);
289                serializer.endTag(ns, "Feature");
290            }
291        }
292        serializer.endTag(ns, "FeatureInfo");
293    }
294
295    /**
296     * Prints XML data indicating what particular processes of interest were running on the device.
297     * It parses a string from the rootProcesses argument that is in the form of
298     * "processName1;processName2;..." with a trailing semi-colon.
299     *
300     * <pre>
301     *   <ProcessInfo>
302     *     <Process name="long_cat_viewer" uid="0" />
303     *     ...
304     *   </ProcessInfo>
305     * </pre>
306     *
307     * @param document
308     * @param parentNode
309     * @param deviceInfo
310     */
311    private void serializeProcessInfo(KXmlSerializer serializer, String rootProcesses)
312            throws IOException {
313        serializer.startTag(ns, "ProcessInfo");
314
315        if (rootProcesses == null) {
316            rootProcesses = "";
317        }
318
319        String[] processNames = rootProcesses.split(";");
320        for (String processName : processNames) {
321            processName = processName.trim();
322            if (processName.length() > 0) {
323                serializer.startTag(ns, "Process");
324                serializer.attribute(ns, "name", processName);
325                serializer.attribute(ns, "uid", "0");
326                serializer.endTag(ns, "Process");
327            }
328        }
329        serializer.endTag(ns, "ProcessInfo");
330    }
331
332    /**
333     * Finds the {@link TestRunResult} with the given name.
334     *
335     * @param runName
336     * @return the {@link TestRunResult}
337     */
338    private TestRunResult findRunResult(String runName) {
339        for (TestRunResult runResult : getRunResults()) {
340            if (runResult.getName().equals(runName)) {
341                return runResult;
342            }
343        }
344        return null;
345    }
346
347    /**
348     * Output the host info XML.
349     *
350     * @param serializer
351     */
352    private void serializeHostInfo(KXmlSerializer serializer) throws IOException {
353        serializer.startTag(ns, "HostInfo");
354
355        String hostName = "";
356        try {
357            hostName = InetAddress.getLocalHost().getHostName();
358        } catch (UnknownHostException ignored) {}
359        serializer.attribute(ns, "name", hostName);
360
361        serializer.startTag(ns, "Os");
362        serializer.attribute(ns, "name", System.getProperty("os.name"));
363        serializer.attribute(ns, "version", System.getProperty("os.version"));
364        serializer.attribute(ns, "arch", System.getProperty("os.arch"));
365        serializer.endTag(ns, "Os");
366
367        serializer.startTag(ns, "Java");
368        serializer.attribute(ns, "name", System.getProperty("java.vendor"));
369        serializer.attribute(ns, "version", System.getProperty("java.version"));
370        serializer.endTag(ns, "Java");
371
372        serializer.startTag(ns, "Cts");
373        serializer.attribute(ns, "version", CTS_VERSION);
374        // TODO: consider outputting tradefed options here
375        serializer.endTag(ns, "Cts");
376
377        serializer.endTag(ns, "HostInfo");
378    }
379
380    /**
381     * Output the test summary XML containing summary totals for all tests.
382     *
383     * @param serializer
384     * @throws IOException
385     */
386    private void serializeTestSummary(KXmlSerializer serializer) throws IOException {
387        serializer.startTag(ns, "Summary");
388        serializer.attribute(ns, "failed", Integer.toString(getNumErrorTests() +
389                getNumFailedTests()));
390        // TODO: output notExecuted, timeout, and omitted count
391        serializer.attribute(ns, "notExecuted", "0");
392        serializer.attribute(ns, "timeout", "0");
393        serializer.attribute(ns, "omitted", "0");
394        serializer.attribute(ns, "pass", Integer.toString(getNumPassedTests()));
395        serializer.attribute(ns, "total", Integer.toString(getNumTotalTests()));
396        serializer.endTag(ns, "Summary");
397    }
398
399    /**
400     * Output the detailed test results XML.
401     *
402     * @param serializer
403     * @throws IOException
404     */
405    private void serializeTestResults(KXmlSerializer serializer) throws IOException {
406        for (TestRunResult runResult : getRunResults()) {
407            serializeTestRunResult(serializer, runResult);
408        }
409    }
410
411    /**
412     * Output the XML for one test run aka test package.
413     *
414     * @param serializer
415     * @param runResult the {@link TestRunResult}
416     * @throws IOException
417     */
418    private void serializeTestRunResult(KXmlSerializer serializer, TestRunResult runResult)
419            throws IOException {
420        if (runResult.getName().equals(DeviceInfoCollector.APP_PACKAGE_NAME)) {
421            // ignore run results for the info collecting packages
422            return;
423        }
424        serializer.startTag(ns, "TestPackage");
425        serializer.attribute(ns, "name", runResult.getName());
426        serializer.attribute(ns, "runTime", formatElapsedTime(runResult.getElapsedTime()));
427        // TODO: generate digest
428        serializer.attribute(ns, "digest", "");
429        serializer.attribute(ns, "failed", Integer.toString(runResult.getNumErrorTests() +
430                runResult.getNumFailedTests()));
431        // TODO: output notExecuted, timeout, and omitted count
432        serializer.attribute(ns, "notExecuted", "0");
433        serializer.attribute(ns, "timeout", "0");
434        serializer.attribute(ns, "omitted", "0");
435        serializer.attribute(ns, "pass", Integer.toString(runResult.getNumPassedTests()));
436        serializer.attribute(ns, "total", Integer.toString(runResult.getNumTests()));
437
438        // the results XML needs to organize test's by class. Build a nested data structure that
439        // group's the results by class name
440        Map<String, Map<TestIdentifier, TestResult>> classResultsMap = buildClassNameMap(
441                runResult.getTestResults());
442
443        for (Map.Entry<String, Map<TestIdentifier, TestResult>> resultsEntry :
444                classResultsMap.entrySet()) {
445            serializer.startTag(ns, "TestCase");
446            serializer.attribute(ns, "name", resultsEntry.getKey());
447            serializeTests(serializer, resultsEntry.getValue());
448            serializer.endTag(ns, "TestCase");
449        }
450        serializer.endTag(ns, "TestPackage");
451    }
452
453    /**
454     * Organizes the test run results into a format organized by class name.
455     */
456    private Map<String, Map<TestIdentifier, TestResult>> buildClassNameMap(
457            Map<TestIdentifier, TestResult> results) {
458        // use a linked hashmap to have predictable iteration order
459        Map<String, Map<TestIdentifier, TestResult>> classResultMap =
460            new LinkedHashMap<String, Map<TestIdentifier, TestResult>>();
461        for (Map.Entry<TestIdentifier, TestResult> resultEntry : results.entrySet()) {
462            String className = resultEntry.getKey().getClassName();
463            Map<TestIdentifier, TestResult> resultsForClass = classResultMap.get(className);
464            if (resultsForClass == null) {
465                resultsForClass = new LinkedHashMap<TestIdentifier, TestResult>();
466                classResultMap.put(className, resultsForClass);
467            }
468            resultsForClass.put(resultEntry.getKey(), resultEntry.getValue());
469        }
470        return classResultMap;
471    }
472
473    /**
474     * Output XML for given map of tests their results
475     *
476     * @param serializer
477     * @param results
478     * @throws IOException
479     */
480    private void serializeTests(KXmlSerializer serializer, Map<TestIdentifier, TestResult> results)
481            throws IOException {
482        for (Map.Entry<TestIdentifier, TestResult> resultEntry : results.entrySet()) {
483            serializeTest(serializer, resultEntry.getKey(), resultEntry.getValue());
484        }
485    }
486
487    /**
488     * Output the XML for given test and result.
489     *
490     * @param serializer
491     * @param testId
492     * @param result
493     * @throws IOException
494     */
495    private void serializeTest(KXmlSerializer serializer, TestIdentifier testId, TestResult result)
496            throws IOException {
497        serializer.startTag(ns, "Test");
498        serializer.attribute(ns, "name", testId.getTestName());
499        serializer.attribute(ns, "result", convertStatus(result.getStatus()));
500
501        if (result.getStackTrace() != null) {
502            String sanitizedStack = sanitizeStackTrace(result.getStackTrace());
503            serializer.startTag(ns, "FailedScene");
504            serializer.attribute(ns, "message", getFailureMessageFromStackTrace(sanitizedStack));
505            serializer.text(sanitizedStack);
506            serializer.endTag(ns, "FailedScene");
507        }
508        serializer.endTag(ns, "Test");
509    }
510
511    /**
512     * Convert a {@link TestStatus} to the result text to output in XML
513     *
514     * @param status the {@link TestStatus}
515     * @return
516     */
517    private String convertStatus(TestStatus status) {
518        switch (status) {
519            case ERROR:
520                return "fail";
521            case FAILURE:
522                return "fail";
523            case PASSED:
524                return "pass";
525            // TODO add notExecuted, omitted timeout
526        }
527        return "omitted";
528    }
529
530    /**
531     * Strip out any invalid XML characters that might cause the report to be unviewable.
532     * http://www.w3.org/TR/REC-xml/#dt-character
533     */
534    private static String sanitizeStackTrace(String trace) {
535        if (trace != null) {
536            return trace.replaceAll("[^\\u0009\\u000A\\u000D\\u0020-\\uD7FF\\uE000-\\uFFFD]", "");
537        } else {
538            return null;
539        }
540    }
541
542    private static String getFailureMessageFromStackTrace(String stack) {
543        // This is probably too simplistic to work in all cases, but for now, just return first
544        // line of stack as failure message
545        int firstNewLine = stack.indexOf('\n');
546        if (firstNewLine != -1) {
547            return stack.substring(0, firstNewLine);
548        }
549        return stack;
550    }
551
552    /**
553     * Return the current timestamp as a {@link String} suitable for displaying.
554     * <p/>
555     * Example: Fri Aug 20 15:13:03 PDT 2010
556     */
557    String getTimestamp() {
558        SimpleDateFormat dateFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy");
559        return dateFormat.format(new Date());
560    }
561
562    /**
563     * Return the current timestamp in a compressed format, used to uniquely identify results.
564     * <p/>
565     * Example: 2010.08.16_11.42.12
566     */
567    private String getResultTimestamp() {
568        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd_HH.mm.ss");
569        return dateFormat.format(new Date());
570    }
571
572    /**
573     * Return a prettified version of the given elapsed time
574     * @return
575     */
576    private String formatElapsedTime(long elapsedTimeMs) {
577        long seconds = TimeUnit.MILLISECONDS.toSeconds(elapsedTimeMs) % 60;
578        long minutes = TimeUnit.MILLISECONDS.toMinutes(elapsedTimeMs) % 60;
579        long hours = TimeUnit.MILLISECONDS.toHours(elapsedTimeMs);
580        StringBuilder time = new StringBuilder();
581        if (hours > 0) {
582            time.append(hours);
583            time.append("h ");
584        }
585        if (minutes > 0) {
586            time.append(minutes);
587            time.append("m ");
588        }
589        time.append(seconds);
590        time.append("s");
591
592        return time.toString();
593    }
594
595    /**
596     * Creates the output stream to use for test results. Exposed for mocking.
597     */
598    OutputStream createOutputResultStream(File reportDir) throws IOException {
599        File reportFile = new File(reportDir, TEST_RESULT_FILE_NAME);
600        Log.i(LOG_TAG, String.format("Created xml report file at %s",
601                reportFile.getAbsolutePath()));
602        return new FileOutputStream(reportFile);
603    }
604
605    /**
606     * Copy the xml formatting files stored in this jar to the results directory
607     *
608     * @param resultsDir
609     */
610    private void copyFormattingFiles(File resultsDir) {
611        for (String resultFileName : CTS_RESULT_RESOURCES) {
612            InputStream configStream = getClass().getResourceAsStream(
613                    String.format("/result/%s", resultFileName));
614            if (configStream != null) {
615                File resultFile = new File(resultsDir, resultFileName);
616                try {
617                    FileUtil.writeToFile(configStream, resultFile);
618                } catch (IOException e) {
619                    Log.w(LOG_TAG, String.format("Failed to write %s to file", resultFileName));
620                }
621            } else {
622                Log.w(LOG_TAG, String.format("Failed to load %s from jar", resultFileName));
623            }
624        }
625    }
626
627    /**
628     * Zip the contents of the given results directory.
629     *
630     * @param resultsDir
631     */
632    private void zipResults(File resultsDir) {
633        // TODO: implement this
634    }
635}
636