CtsXmlResultReporter.java revision 98e9f2ce6c8d34b99242c237cdeb3cdb42b3f924
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 android.tests.getinfo.DeviceInfoConstants;
20
21import com.android.cts.tradefed.build.CtsBuildHelper;
22import com.android.cts.tradefed.build.CtsBuildProvider;
23import com.android.cts.tradefed.device.DeviceInfoCollector;
24import com.android.cts.tradefed.testtype.CtsTest;
25import com.android.ddmlib.Log;
26import com.android.ddmlib.Log.LogLevel;
27import com.android.ddmlib.testrunner.TestIdentifier;
28import com.android.tradefed.build.IBuildInfo;
29import com.android.tradefed.build.IFolderBuildInfo;
30import com.android.tradefed.config.Option;
31import com.android.tradefed.log.LogUtil.CLog;
32import com.android.tradefed.result.ITestInvocationListener;
33import com.android.tradefed.result.InputStreamSource;
34import com.android.tradefed.result.LogDataType;
35import com.android.tradefed.result.TestSummary;
36import com.android.tradefed.util.FileUtil;
37import com.android.tradefed.util.StreamUtil;
38
39import org.kxml2.io.KXmlSerializer;
40
41import java.io.File;
42import java.io.FileNotFoundException;
43import java.io.FileOutputStream;
44import java.io.IOException;
45import java.io.InputStream;
46import java.io.OutputStream;
47import java.net.InetAddress;
48import java.net.UnknownHostException;
49import java.util.HashMap;
50import java.util.Map;
51
52/**
53 * Writes results to an XML files in the CTS format.
54 * <p/>
55 * Collects all test info in memory, then dumps to file when invocation is complete.
56 * <p/>
57 * Outputs xml in format governed by the cts_result.xsd
58 */
59public class CtsXmlResultReporter implements ITestInvocationListener {
60
61    private static final String LOG_TAG = "CtsXmlResultReporter";
62
63    static final String TEST_RESULT_FILE_NAME = "testResult.xml";
64    private static final String CTS_RESULT_FILE_VERSION = "1.11";
65    private static final String[] CTS_RESULT_RESOURCES = {"cts_result.xsl", "cts_result.css",
66        "logo.gif", "newrule-green.png"};
67
68    /** the XML namespace */
69    static final String ns = null;
70
71    // XML constants
72    static final String SUMMARY_TAG = "Summary";
73    static final String PASS_ATTR = "pass";
74    static final String TIMEOUT_ATTR = "timeout";
75    static final String NOT_EXECUTED_ATTR = "notExecuted";
76    static final String FAILED_ATTR = "failed";
77    static final String RESULT_TAG = "TestResult";
78    static final String PLAN_ATTR = "testPlan";
79
80    private static final String REPORT_DIR_NAME = "output-file-path";
81    @Option(name=REPORT_DIR_NAME, description="root file system path to directory to store xml " +
82            "test results and associated logs. If not specified, results will be stored at " +
83            "<cts root>/repository/results")
84    protected File mReportDir = null;
85
86    // listen in on the plan option provided to CtsTest
87    @Option(name = CtsTest.PLAN_OPTION, description = "the test plan to run.")
88    private String mPlanName = "NA";
89
90    // listen in on the continue-session option provided to CtsTest
91    @Option(name = CtsTest.CONTINUE_OPTION, description = "the test result session to continue.")
92    private Integer mContinueSessionId = null;
93
94    @Option(name = "quiet-output", description = "Mute display of test results.")
95    private boolean mQuietOutput = false;
96
97    protected IBuildInfo mBuildInfo;
98    private String mStartTime;
99    private String mDeviceSerial;
100    private TestResults mResults = new TestResults();
101    private TestPackageResult mCurrentPkgResult = null;
102
103    public void setReportDir(File reportDir) {
104        mReportDir = reportDir;
105    }
106
107    /**
108     * {@inheritDoc}
109     */
110    @Override
111    public void invocationStarted(IBuildInfo buildInfo) {
112        mBuildInfo = buildInfo;
113        if (!(buildInfo instanceof IFolderBuildInfo)) {
114            throw new IllegalArgumentException("build info is not a IFolderBuildInfo");
115        }
116        IFolderBuildInfo ctsBuild = (IFolderBuildInfo)buildInfo;
117        mDeviceSerial = buildInfo.getDeviceSerial() == null ? "unknown_device" :
118            buildInfo.getDeviceSerial();
119        if (mContinueSessionId != null) {
120            CLog.d("Continuing session %d", mContinueSessionId);
121            // reuse existing directory
122            TestResultRepo resultRepo = new TestResultRepo(getBuildHelper(ctsBuild).getResultsDir());
123            mResults = resultRepo.getResult(mContinueSessionId);
124            if (mResults == null) {
125                throw new IllegalArgumentException(String.format("Could not find session %d",
126                        mContinueSessionId));
127            }
128            mPlanName = resultRepo.getSummaries().get(mContinueSessionId).getTestPlan();
129            mStartTime = resultRepo.getSummaries().get(mContinueSessionId).getTimestamp();
130            mReportDir = resultRepo.getReportDir(mContinueSessionId);
131        } else {
132            if (mReportDir == null) {
133                mReportDir = getBuildHelper(ctsBuild).getResultsDir();
134            }
135            // create a unique directory for saving results, using old cts host convention
136            // TODO: in future, consider using LogFileSaver to create build-specific directories
137            mReportDir = new File(mReportDir, TimeUtil.getResultTimestamp());
138            mReportDir.mkdirs();
139            mStartTime = getTimestamp();
140            logResult("Created result dir %s", mReportDir.getName());
141        }
142    }
143
144    /**
145     * Helper method to retrieve the {@link CtsBuildHelper}.
146     * @param ctsBuild
147     */
148    CtsBuildHelper getBuildHelper(IFolderBuildInfo ctsBuild) {
149        CtsBuildHelper buildHelper = new CtsBuildHelper(ctsBuild.getRootDir());
150        try {
151            buildHelper.validateStructure();
152        } catch (FileNotFoundException e) {
153            throw new IllegalArgumentException("Invalid CTS build", e);
154        }
155        return buildHelper;
156    }
157
158    /**
159     * {@inheritDoc}
160     */
161    @Override
162    public void testLog(String dataName, LogDataType dataType, InputStreamSource dataStream) {
163        // save as zip file in report dir
164        // TODO: ensure uniqueness of file name
165        // TODO: use dataType.getFileExt() when its made public
166        String fileName = String.format("%s.%s", dataName, dataType.name().toLowerCase());
167        // TODO: consider compressing large files
168        File logFile = new File(mReportDir, fileName);
169        try {
170            FileUtil.writeToFile(dataStream.createInputStream(), logFile);
171        } catch (IOException e) {
172            Log.e(LOG_TAG, String.format("Failed to write log %s", logFile.getAbsolutePath()));
173        }
174    }
175
176    /**
177     * {@inheritDoc}
178     */
179    @Override
180    public void testRunStarted(String name, int numTests) {
181        if (mCurrentPkgResult != null && !name.equals(mCurrentPkgResult.getAppPackageName())) {
182            // display results from previous run
183            logCompleteRun(mCurrentPkgResult);
184        }
185        if (name.equals(DeviceInfoCollector.APP_PACKAGE_NAME)) {
186            logResult("Collecting device info");
187        } else  if (mCurrentPkgResult == null || !name.equals(
188                mCurrentPkgResult.getAppPackageName())) {
189            logResult("-----------------------------------------");
190            logResult("Test package %s started", name);
191            logResult("-----------------------------------------");
192        }
193        mCurrentPkgResult = mResults.getOrCreatePackage(name);
194    }
195
196    /**
197     * {@inheritDoc}
198     */
199    @Override
200    public void testStarted(TestIdentifier test) {
201        mCurrentPkgResult.insertTest(test);
202    }
203
204    /**
205     * {@inheritDoc}
206     */
207    @Override
208    public void testFailed(TestFailure status, TestIdentifier test, String trace) {
209        mCurrentPkgResult.reportTestFailure(test, CtsTestStatus.FAIL, trace);
210    }
211
212    /**
213     * {@inheritDoc}
214     */
215    @Override
216    public void testEnded(TestIdentifier test, Map<String, String> testMetrics) {
217        mCurrentPkgResult.reportTestEnded(test);
218        Test result = mCurrentPkgResult.findTest(test);
219        String stack = result.getStackTrace() == null ? "" : "\n" + result.getStackTrace();
220        logResult("%s#%s %s %s", test.getClassName(), test.getTestName(), result.getResult(),
221                stack);
222    }
223
224    /**
225     * {@inheritDoc}
226     */
227    @Override
228    public void testRunEnded(long elapsedTime, Map<String, String> runMetrics) {
229        mCurrentPkgResult.populateMetrics(runMetrics);
230    }
231
232    /**
233     * {@inheritDoc}
234     */
235    @Override
236    public void invocationEnded(long elapsedTime) {
237        // display the results of the last completed run
238        if (mCurrentPkgResult != null) {
239            logCompleteRun(mCurrentPkgResult);
240        }
241        createXmlResult(mReportDir, mStartTime, elapsedTime);
242        copyFormattingFiles(mReportDir);
243        zipResults(mReportDir);
244    }
245
246    private void logResult(String format, Object... args) {
247        if (mQuietOutput) {
248            CLog.i(format, args);
249        } else {
250            Log.logAndDisplay(LogLevel.INFO, mDeviceSerial, String.format(format, args));
251        }
252    }
253
254    private void logCompleteRun(TestPackageResult pkgResult) {
255        if (pkgResult.getAppPackageName().equals(DeviceInfoCollector.APP_PACKAGE_NAME)) {
256            logResult("Device info collection complete");
257            return;
258        }
259        logResult("%s package complete: Passed %d, Failed %d, Not Executed %d",
260                pkgResult.getAppPackageName(), pkgResult.countTests(CtsTestStatus.PASS),
261                pkgResult.countTests(CtsTestStatus.FAIL),
262                pkgResult.countTests(CtsTestStatus.NOT_EXECUTED));
263    }
264
265    /**
266     * Creates a report file and populates it with the report data from the completed tests.
267     */
268    private void createXmlResult(File reportDir, String startTimestamp, long elapsedTime) {
269        String endTime = getTimestamp();
270
271        OutputStream stream = null;
272        try {
273            stream = createOutputResultStream(reportDir);
274            KXmlSerializer serializer = new KXmlSerializer();
275            serializer.setOutput(stream, "UTF-8");
276            serializer.startDocument("UTF-8", false);
277            serializer.setFeature(
278                    "http://xmlpull.org/v1/doc/features.html#indent-output", true);
279            serializer.processingInstruction("xml-stylesheet type=\"text/xsl\"  " +
280                    "href=\"cts_result.xsl\"");
281            serializeResultsDoc(serializer, startTimestamp, endTime);
282            serializer.endDocument();
283            String msg = String.format("XML test result file generated at %s. Passed %d, " +
284                    "Failed %d, Not Executed %d", mReportDir.getName(),
285                    mResults.countTests(CtsTestStatus.PASS),
286                    mResults.countTests(CtsTestStatus.FAIL),
287                    mResults.countTests(CtsTestStatus.NOT_EXECUTED));
288            logResult(msg);
289            logResult("Time: %s", TimeUtil.formatElapsedTime(elapsedTime));
290        } catch (IOException e) {
291            Log.e(LOG_TAG, "Failed to generate report data");
292        } finally {
293            StreamUtil.closeStream(stream);
294        }
295    }
296
297    /**
298     * Output the results XML.
299     *
300     * @param serializer the {@link KXmlSerializer} to use
301     * @param startTime the user-friendly starting time of the test invocation
302     * @param endTime the user-friendly ending time of the test invocation
303     * @throws IOException
304     */
305    private void serializeResultsDoc(KXmlSerializer serializer, String startTime, String endTime)
306            throws IOException {
307        serializer.startTag(ns, RESULT_TAG);
308        serializer.attribute(ns, PLAN_ATTR, mPlanName);
309        serializer.attribute(ns, "starttime", startTime);
310        serializer.attribute(ns, "endtime", endTime);
311        serializer.attribute(ns, "version", CTS_RESULT_FILE_VERSION);
312
313        serializeDeviceInfo(serializer);
314        serializeHostInfo(serializer);
315        serializeTestSummary(serializer);
316        mResults.serialize(serializer);
317        // TODO: not sure why, but the serializer doesn't like this statement
318        //serializer.endTag(ns, RESULT_TAG);
319    }
320
321    /**
322     * Output the device info XML.
323     *
324     * @param serializer
325     */
326    private void serializeDeviceInfo(KXmlSerializer serializer) throws IOException {
327        serializer.startTag(ns, "DeviceInfo");
328
329        Map<String, String> deviceInfoMetrics = mResults.getDeviceInfoMetrics();
330        if (deviceInfoMetrics == null || deviceInfoMetrics.isEmpty()) {
331            // this might be expected, if device info collection was turned off
332            CLog.d("Could not find device info");
333            return;
334        }
335
336        // Extract metrics that need extra handling, and then dump the remainder into BuildInfo
337        Map<String, String> metricsCopy = new HashMap<String, String>(
338                deviceInfoMetrics);
339        serializer.startTag(ns, "Screen");
340        String screenWidth = metricsCopy.remove(DeviceInfoConstants.SCREEN_WIDTH);
341        String screenHeight = metricsCopy.remove(DeviceInfoConstants.SCREEN_HEIGHT);
342        serializer.attribute(ns, "resolution", String.format("%sx%s", screenWidth, screenHeight));
343        serializer.attribute(ns, DeviceInfoConstants.SCREEN_DENSITY,
344                metricsCopy.remove(DeviceInfoConstants.SCREEN_DENSITY));
345        serializer.attribute(ns, DeviceInfoConstants.SCREEN_DENSITY_BUCKET,
346                metricsCopy.remove(DeviceInfoConstants.SCREEN_DENSITY_BUCKET));
347        serializer.attribute(ns, DeviceInfoConstants.SCREEN_SIZE,
348                metricsCopy.remove(DeviceInfoConstants.SCREEN_SIZE));
349        serializer.endTag(ns, "Screen");
350
351        serializer.startTag(ns, "PhoneSubInfo");
352        serializer.attribute(ns, "subscriberId", metricsCopy.remove(
353                DeviceInfoConstants.PHONE_NUMBER));
354        serializer.endTag(ns, "PhoneSubInfo");
355
356        String featureData = metricsCopy.remove(DeviceInfoConstants.FEATURES);
357        String processData = metricsCopy.remove(DeviceInfoConstants.PROCESSES);
358
359        // dump the remaining metrics without translation
360        serializer.startTag(ns, "BuildInfo");
361        for (Map.Entry<String, String> metricEntry : metricsCopy.entrySet()) {
362            serializer.attribute(ns, metricEntry.getKey(), metricEntry.getValue());
363        }
364        serializer.attribute(ns, "deviceID", mDeviceSerial);
365        serializer.endTag(ns, "BuildInfo");
366
367        serializeFeatureInfo(serializer, featureData);
368        serializeProcessInfo(serializer, processData);
369
370        serializer.endTag(ns, "DeviceInfo");
371    }
372
373    /**
374     * Prints XML indicating what features are supported by the device. It parses a string from the
375     * featureData argument that is in the form of "feature1:true;feature2:false;featuer3;true;"
376     * with a trailing semi-colon.
377     *
378     * <pre>
379     *  <FeatureInfo>
380     *     <Feature name="android.name.of.feature" available="true" />
381     *     ...
382     *   </FeatureInfo>
383     * </pre>
384     *
385     * @param serializer used to create XML
386     * @param featureData raw unparsed feature data
387     */
388    private void serializeFeatureInfo(KXmlSerializer serializer, String featureData) throws IOException {
389        serializer.startTag(ns, "FeatureInfo");
390
391        if (featureData == null) {
392            featureData = "";
393        }
394
395        String[] featurePairs = featureData.split(";");
396        for (String featurePair : featurePairs) {
397            String[] nameTypeAvailability = featurePair.split(":");
398            if (nameTypeAvailability.length >= 3) {
399                serializer.startTag(ns, "Feature");
400                serializer.attribute(ns, "name", nameTypeAvailability[0]);
401                serializer.attribute(ns, "type", nameTypeAvailability[1]);
402                serializer.attribute(ns, "available", nameTypeAvailability[2]);
403                serializer.endTag(ns, "Feature");
404            }
405        }
406        serializer.endTag(ns, "FeatureInfo");
407    }
408
409    /**
410     * Prints XML data indicating what particular processes of interest were running on the device.
411     * It parses a string from the rootProcesses argument that is in the form of
412     * "processName1;processName2;..." with a trailing semi-colon.
413     *
414     * <pre>
415     *   <ProcessInfo>
416     *     <Process name="long_cat_viewer" uid="0" />
417     *     ...
418     *   </ProcessInfo>
419     * </pre>
420     */
421    private void serializeProcessInfo(KXmlSerializer serializer, String rootProcesses)
422            throws IOException {
423        serializer.startTag(ns, "ProcessInfo");
424
425        if (rootProcesses == null) {
426            rootProcesses = "";
427        }
428
429        String[] processNames = rootProcesses.split(";");
430        for (String processName : processNames) {
431            processName = processName.trim();
432            if (processName.length() > 0) {
433                serializer.startTag(ns, "Process");
434                serializer.attribute(ns, "name", processName);
435                serializer.attribute(ns, "uid", "0");
436                serializer.endTag(ns, "Process");
437            }
438        }
439        serializer.endTag(ns, "ProcessInfo");
440    }
441
442    /**
443     * Output the host info XML.
444     *
445     * @param serializer
446     */
447    private void serializeHostInfo(KXmlSerializer serializer) throws IOException {
448        serializer.startTag(ns, "HostInfo");
449
450        String hostName = "";
451        try {
452            hostName = InetAddress.getLocalHost().getHostName();
453        } catch (UnknownHostException ignored) {}
454        serializer.attribute(ns, "name", hostName);
455
456        serializer.startTag(ns, "Os");
457        serializer.attribute(ns, "name", System.getProperty("os.name"));
458        serializer.attribute(ns, "version", System.getProperty("os.version"));
459        serializer.attribute(ns, "arch", System.getProperty("os.arch"));
460        serializer.endTag(ns, "Os");
461
462        serializer.startTag(ns, "Java");
463        serializer.attribute(ns, "name", System.getProperty("java.vendor"));
464        serializer.attribute(ns, "version", System.getProperty("java.version"));
465        serializer.endTag(ns, "Java");
466
467        serializer.startTag(ns, "Cts");
468        serializer.attribute(ns, "version", CtsBuildProvider.CTS_BUILD_VERSION);
469        // TODO: consider outputting other tradefed options here
470        serializer.startTag(ns, "IntValue");
471        serializer.attribute(ns, "name", "testStatusTimeoutMs");
472        // TODO: create a constant variable for testStatusTimeoutMs value. Currently it cannot be
473        // changed
474        serializer.attribute(ns, "value", "600000");
475        serializer.endTag(ns, "IntValue");
476        serializer.endTag(ns, "Cts");
477
478        serializer.endTag(ns, "HostInfo");
479    }
480
481    /**
482     * Output the test summary XML containing summary totals for all tests.
483     *
484     * @param serializer
485     * @throws IOException
486     */
487    private void serializeTestSummary(KXmlSerializer serializer) throws IOException {
488        serializer.startTag(ns, SUMMARY_TAG);
489        serializer.attribute(ns, FAILED_ATTR, Integer.toString(mResults.countTests(
490                CtsTestStatus.FAIL)));
491        serializer.attribute(ns, NOT_EXECUTED_ATTR,  Integer.toString(mResults.countTests(
492                CtsTestStatus.NOT_EXECUTED)));
493        // ignore timeouts - these are reported as errors
494        serializer.attribute(ns, TIMEOUT_ATTR, "0");
495        serializer.attribute(ns, PASS_ATTR, Integer.toString(mResults.countTests(
496                CtsTestStatus.PASS)));
497        serializer.endTag(ns, SUMMARY_TAG);
498    }
499
500    /**
501     * Creates the output stream to use for test results. Exposed for mocking.
502     */
503    OutputStream createOutputResultStream(File reportDir) throws IOException {
504        File reportFile = new File(reportDir, TEST_RESULT_FILE_NAME);
505        logResult("Created xml report file at file://%s", reportFile.getAbsolutePath());
506        return new FileOutputStream(reportFile);
507    }
508
509    /**
510     * Copy the xml formatting files stored in this jar to the results directory
511     *
512     * @param resultsDir
513     */
514    private void copyFormattingFiles(File resultsDir) {
515        for (String resultFileName : CTS_RESULT_RESOURCES) {
516            InputStream configStream = getClass().getResourceAsStream(String.format("/%s",
517                    resultFileName));
518            if (configStream != null) {
519                File resultFile = new File(resultsDir, resultFileName);
520                try {
521                    FileUtil.writeToFile(configStream, resultFile);
522                } catch (IOException e) {
523                    Log.w(LOG_TAG, String.format("Failed to write %s to file", resultFileName));
524                }
525            } else {
526                Log.w(LOG_TAG, String.format("Failed to load %s from jar", resultFileName));
527            }
528        }
529    }
530
531    /**
532     * Zip the contents of the given results directory.
533     *
534     * @param resultsDir
535     */
536    private void zipResults(File resultsDir) {
537        try {
538            // create a file in parent directory, with same name as resultsDir
539            File zipResultFile = new File(resultsDir.getParent(), String.format("%s.zip",
540                    resultsDir.getName()));
541            FileUtil.createZip(resultsDir, zipResultFile);
542        } catch (IOException e) {
543            Log.w(LOG_TAG, String.format("Failed to create zip for %s", resultsDir.getName()));
544        }
545    }
546
547    /**
548     * Get a String version of the current time.
549     * <p/>
550     * Exposed so unit tests can mock.
551     */
552    String getTimestamp() {
553        return TimeUtil.getTimestamp();
554    }
555
556    /**
557     * {@inheritDoc}
558     */
559    @Override
560    public void testRunFailed(String errorMessage) {
561        // ignore
562    }
563
564    /**
565     * {@inheritDoc}
566     */
567    @Override
568    public void testRunStopped(long elapsedTime) {
569        // ignore
570    }
571
572    /**
573     * {@inheritDoc}
574     */
575    @Override
576    public void invocationFailed(Throwable cause) {
577        // ignore
578    }
579
580    /**
581     * {@inheritDoc}
582     */
583    @Override
584    public TestSummary getSummary() {
585        return null;
586    }
587}
588