CtsXmlResultReporter.java revision 504fc8769168ed9519f5752748e50df4ecc2c885
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.build.CtsBuildHelper;
20import com.android.cts.tradefed.device.DeviceInfoCollector;
21import com.android.cts.tradefed.testtype.CtsTest;
22import com.android.cts.tradefed.util.CtsHostStore;
23import com.android.ddmlib.Log;
24import com.android.ddmlib.Log.LogLevel;
25import com.android.ddmlib.testrunner.TestIdentifier;
26import com.android.tradefed.build.IBuildInfo;
27import com.android.tradefed.build.IFolderBuildInfo;
28import com.android.tradefed.config.Option;
29import com.android.tradefed.log.LogUtil.CLog;
30import com.android.tradefed.result.ITestInvocationListener;
31import com.android.tradefed.result.InputStreamSource;
32import com.android.tradefed.result.LogDataType;
33import com.android.tradefed.result.LogFileSaver;
34import com.android.tradefed.result.TestSummary;
35import com.android.tradefed.util.FileUtil;
36import com.android.tradefed.util.StreamUtil;
37
38import org.kxml2.io.KXmlSerializer;
39
40import java.io.File;
41import java.io.FileNotFoundException;
42import java.io.FileOutputStream;
43import java.io.IOException;
44import java.io.InputStream;
45import java.io.OutputStream;
46import java.util.Map;
47import java.util.regex.Matcher;
48import java.util.regex.Pattern;
49
50/**
51 * Writes results to an XML files in the CTS format.
52 * <p/>
53 * Collects all test info in memory, then dumps to file when invocation is complete.
54 * <p/>
55 * Outputs xml in format governed by the cts_result.xsd
56 */
57public class CtsXmlResultReporter implements ITestInvocationListener {
58    private static final String LOG_TAG = "CtsXmlResultReporter";
59
60    static final String TEST_RESULT_FILE_NAME = "testResult.xml";
61    static final String CTS_RESULT_FILE_VERSION = "4.4";
62    private static final String[] CTS_RESULT_RESOURCES = {"cts_result.xsl", "cts_result.css",
63        "logo.gif", "newrule-green.png"};
64
65    /** the XML namespace */
66    static final String ns = null;
67
68    static final String RESULT_TAG = "TestResult";
69    static final String PLAN_ATTR = "testPlan";
70    static final String STARTTIME_ATTR = "starttime";
71
72    private static final String REPORT_DIR_NAME = "output-file-path";
73    @Option(name=REPORT_DIR_NAME, description="root file system path to directory to store xml " +
74            "test results and associated logs. If not specified, results will be stored at " +
75            "<cts root>/repository/results")
76    protected File mReportDir = null;
77
78    // listen in on the plan option provided to CtsTest
79    @Option(name = CtsTest.PLAN_OPTION, description = "the test plan to run.")
80    private String mPlanName = "NA";
81
82    // listen in on the continue-session option provided to CtsTest
83    @Option(name = CtsTest.CONTINUE_OPTION, description = "the test result session to continue.")
84    private Integer mContinueSessionId = null;
85
86    @Option(name = "quiet-output", description = "Mute display of test results.")
87    private boolean mQuietOutput = false;
88
89    @Option(name = "result-server", description = "Server to publish test results.")
90    private String mResultServer;
91
92    protected IBuildInfo mBuildInfo;
93    private String mStartTime;
94    private String mDeviceSerial;
95    private TestResults mResults = new TestResults();
96    private TestPackageResult mCurrentPkgResult = null;
97    private boolean mIsDeviceInfoRun = false;
98    private ResultReporter mReporter;
99    private File mLogDir;
100    private String mSuiteName;
101
102    private static final Pattern mCtsLogPattern = Pattern.compile("(.*)\\+\\+\\+\\+(.*)");
103
104    public void setReportDir(File reportDir) {
105        mReportDir = reportDir;
106    }
107
108    /**
109     * {@inheritDoc}
110     */
111    @Override
112    public void invocationStarted(IBuildInfo buildInfo) {
113        mBuildInfo = buildInfo;
114        if (!(buildInfo instanceof IFolderBuildInfo)) {
115            throw new IllegalArgumentException("build info is not a IFolderBuildInfo");
116        }
117        IFolderBuildInfo ctsBuild = (IFolderBuildInfo)buildInfo;
118        CtsBuildHelper ctsBuildHelper = getBuildHelper(ctsBuild);
119        mDeviceSerial = buildInfo.getDeviceSerial() == null ? "unknown_device" :
120            buildInfo.getDeviceSerial();
121        if (mContinueSessionId != null) {
122            CLog.d("Continuing session %d", mContinueSessionId);
123            // reuse existing directory
124            TestResultRepo resultRepo = new TestResultRepo(ctsBuildHelper.getResultsDir());
125            mResults = resultRepo.getResult(mContinueSessionId);
126            if (mResults == null) {
127                throw new IllegalArgumentException(String.format("Could not find session %d",
128                        mContinueSessionId));
129            }
130            mPlanName = resultRepo.getSummaries().get(mContinueSessionId).getTestPlan();
131            mStartTime = resultRepo.getSummaries().get(mContinueSessionId).getStartTime();
132            mReportDir = resultRepo.getReportDir(mContinueSessionId);
133        } else {
134            if (mReportDir == null) {
135                mReportDir = ctsBuildHelper.getResultsDir();
136            }
137            mReportDir = createUniqueReportDir(mReportDir);
138
139            mStartTime = getTimestamp();
140            logResult("Created result dir %s", mReportDir.getName());
141        }
142        mSuiteName = ctsBuildHelper.getSuiteName();
143        mReporter = new ResultReporter(mResultServer, mSuiteName);
144
145        // TODO: allow customization of log dir
146        // create a unique directory for saving logs, with same name as result dir
147        File rootLogDir = getBuildHelper(ctsBuild).getLogsDir();
148        mLogDir = new File(rootLogDir, mReportDir.getName());
149        mLogDir.mkdirs();
150    }
151
152    /**
153     * Create a unique directory for saving results.
154     * <p/>
155     * Currently using legacy CTS host convention of timestamp directory names. In case of
156     * collisions, will use {@link FileUtil} to generate unique file name.
157     * <p/>
158     * TODO: in future, consider using LogFileSaver to create build-specific directories
159     *
160     * @param parentDir the parent folder to create dir in
161     * @return the created directory
162     */
163    private static synchronized File createUniqueReportDir(File parentDir) {
164        // TODO: in future, consider using LogFileSaver to create build-specific directories
165
166        File reportDir = new File(parentDir, TimeUtil.getResultTimestamp());
167        if (reportDir.exists()) {
168            // directory with this timestamp exists already! Choose a unique, although uglier, name
169            try {
170                reportDir = FileUtil.createTempDir(TimeUtil.getResultTimestamp() + "_", parentDir);
171            } catch (IOException e) {
172                CLog.e(e);
173                CLog.e("Failed to create result directory %s", reportDir.getAbsolutePath());
174            }
175        } else {
176            if (!reportDir.mkdirs()) {
177                // TODO: consider throwing an exception
178                CLog.e("mkdirs failed when attempting to create result directory %s",
179                        reportDir.getAbsolutePath());
180            }
181        }
182        return reportDir;
183    }
184
185    /**
186     * Helper method to retrieve the {@link CtsBuildHelper}.
187     * @param ctsBuild
188     */
189    CtsBuildHelper getBuildHelper(IFolderBuildInfo ctsBuild) {
190        CtsBuildHelper buildHelper = new CtsBuildHelper(ctsBuild.getRootDir());
191        try {
192            buildHelper.validateStructure();
193        } catch (FileNotFoundException e) {
194            // just log an error - it might be expected if we failed to retrieve a build
195            CLog.e("Invalid CTS build %s", ctsBuild.getRootDir());
196        }
197        return buildHelper;
198    }
199
200    /**
201     * {@inheritDoc}
202     */
203    @Override
204    public void testLog(String dataName, LogDataType dataType, InputStreamSource dataStream) {
205        try {
206            File logFile = getLogFileSaver().saveAndZipLogData(dataName, dataType,
207                    dataStream.createInputStream());
208            logResult(String.format("Saved log %s", logFile.getName()));
209        } catch (IOException e) {
210            CLog.e("Failed to write log for %s", dataName);
211        }
212    }
213
214    /**
215     * Return the {@link LogFileSaver} to use.
216     * <p/>
217     * Exposed for unit testing.
218     */
219    LogFileSaver getLogFileSaver() {
220        return new LogFileSaver(mLogDir);
221    }
222
223    /**
224     * {@inheritDoc}
225     */
226    @Override
227    public void testRunStarted(String name, int numTests) {
228        if (mCurrentPkgResult != null && !name.equals(mCurrentPkgResult.getAppPackageName())) {
229            // display results from previous run
230            logCompleteRun(mCurrentPkgResult);
231        }
232        mIsDeviceInfoRun = name.equals(DeviceInfoCollector.APP_PACKAGE_NAME);
233        if (mIsDeviceInfoRun) {
234            logResult("Collecting device info");
235        } else  {
236            if (mCurrentPkgResult == null || !name.equals(mCurrentPkgResult.getAppPackageName())) {
237                logResult("-----------------------------------------");
238                logResult("Test package %s started", name);
239                logResult("-----------------------------------------");
240            }
241            mCurrentPkgResult = mResults.getOrCreatePackage(name);
242        }
243
244    }
245
246    /**
247     * {@inheritDoc}
248     */
249    @Override
250    public void testStarted(TestIdentifier test) {
251        mCurrentPkgResult.insertTest(test);
252    }
253
254    /**
255     * {@inheritDoc}
256     */
257    @Override
258    public void testFailed(TestFailure status, TestIdentifier test, String trace) {
259        mCurrentPkgResult.reportTestFailure(test, CtsTestStatus.FAIL, trace);
260    }
261
262    /**
263     * {@inheritDoc}
264     */
265    @Override
266    public void testEnded(TestIdentifier test, Map<String, String> testMetrics) {
267        collectCtsResults(test, testMetrics);
268        mCurrentPkgResult.reportTestEnded(test);
269        Test result = mCurrentPkgResult.findTest(test);
270        String stack = result.getStackTrace() == null ? "" : "\n" + result.getStackTrace();
271        logResult("%s#%s %s %s", test.getClassName(), test.getTestName(), result.getResult(),
272                stack);
273    }
274
275    /**
276     * Collect Cts results for both device and host tests to the package result.
277     * @param test test ran
278     * @param testMetrics test metrics which can contain performance result for device tests
279     */
280    private void collectCtsResults(TestIdentifier test, Map<String, String> testMetrics) {
281        // device test can have performance results in testMetrics
282        String perfResult = CtsReportUtil.getCtsResultFromMetrics(testMetrics);
283        // host test should be checked in CtsHostStore.
284        if (perfResult == null) {
285            perfResult = CtsHostStore.removeCtsResult(mDeviceSerial, test.toString());
286        }
287        if (perfResult != null) {
288            // CTS result is passed in Summary++++Details format.
289            // Extract Summary and Details, and pass them.
290            Matcher m = mCtsLogPattern.matcher(perfResult);
291            if (m.find()) {
292                mCurrentPkgResult.reportPerformanceResult(test, CtsTestStatus.PASS, m.group(1),
293                        m.group(2));
294            } else {
295                logResult("CTS Result unrecognizable:" + perfResult);
296            }
297        }
298    }
299
300    /**
301     * {@inheritDoc}
302     */
303    @Override
304    public void testRunEnded(long elapsedTime, Map<String, String> runMetrics) {
305        if (mIsDeviceInfoRun) {
306            mResults.populateDeviceInfoMetrics(runMetrics);
307        } else {
308            mCurrentPkgResult.populateMetrics(runMetrics);
309        }
310    }
311
312    /**
313     * {@inheritDoc}
314     */
315    @Override
316    public void invocationEnded(long elapsedTime) {
317        // display the results of the last completed run
318        if (mCurrentPkgResult != null) {
319            logCompleteRun(mCurrentPkgResult);
320        }
321        if (mReportDir == null || mStartTime == null) {
322            // invocationStarted must have failed, abort
323            CLog.w("Unable to create XML report");
324            return;
325        }
326
327        File reportFile = getResultFile(mReportDir);
328        createXmlResult(reportFile, mStartTime, elapsedTime);
329        copyFormattingFiles(mReportDir);
330        zipResults(mReportDir);
331
332        try {
333            mReporter.reportResult(reportFile);
334        } catch (IOException e) {
335            CLog.e(e);
336        }
337    }
338
339    private void logResult(String format, Object... args) {
340        if (mQuietOutput) {
341            CLog.i(format, args);
342        } else {
343            Log.logAndDisplay(LogLevel.INFO, mDeviceSerial, String.format(format, args));
344        }
345    }
346
347    private void logCompleteRun(TestPackageResult pkgResult) {
348        if (pkgResult.getAppPackageName().equals(DeviceInfoCollector.APP_PACKAGE_NAME)) {
349            logResult("Device info collection complete");
350            return;
351        }
352        logResult("%s package complete: Passed %d, Failed %d, Not Executed %d",
353                pkgResult.getAppPackageName(), pkgResult.countTests(CtsTestStatus.PASS),
354                pkgResult.countTests(CtsTestStatus.FAIL),
355                pkgResult.countTests(CtsTestStatus.NOT_EXECUTED));
356    }
357
358    /**
359     * Creates a report file and populates it with the report data from the completed tests.
360     */
361    private void createXmlResult(File reportFile, String startTimestamp, long elapsedTime) {
362        String endTime = getTimestamp();
363        OutputStream stream = null;
364        try {
365            stream = createOutputResultStream(reportFile);
366            KXmlSerializer serializer = new KXmlSerializer();
367            serializer.setOutput(stream, "UTF-8");
368            serializer.startDocument("UTF-8", false);
369            serializer.setFeature(
370                    "http://xmlpull.org/v1/doc/features.html#indent-output", true);
371            serializer.processingInstruction("xml-stylesheet type=\"text/xsl\"  " +
372                    "href=\"cts_result.xsl\"");
373            serializeResultsDoc(serializer, startTimestamp, endTime);
374            serializer.endDocument();
375            String msg = String.format("XML test result file generated at %s. Passed %d, " +
376                    "Failed %d, Not Executed %d", mReportDir.getName(),
377                    mResults.countTests(CtsTestStatus.PASS),
378                    mResults.countTests(CtsTestStatus.FAIL),
379                    mResults.countTests(CtsTestStatus.NOT_EXECUTED));
380            logResult(msg);
381            logResult("Time: %s", TimeUtil.formatElapsedTime(elapsedTime));
382        } catch (IOException e) {
383            Log.e(LOG_TAG, "Failed to generate report data");
384        } finally {
385            StreamUtil.closeStream(stream);
386        }
387    }
388
389    /**
390     * Output the results XML.
391     *
392     * @param serializer the {@link KXmlSerializer} to use
393     * @param startTime the user-friendly starting time of the test invocation
394     * @param endTime the user-friendly ending time of the test invocation
395     * @throws IOException
396     */
397    private void serializeResultsDoc(KXmlSerializer serializer, String startTime, String endTime)
398            throws IOException {
399        serializer.startTag(ns, RESULT_TAG);
400        serializer.attribute(ns, PLAN_ATTR, mPlanName);
401        serializer.attribute(ns, STARTTIME_ATTR, startTime);
402        serializer.attribute(ns, "endtime", endTime);
403        serializer.attribute(ns, "version", CTS_RESULT_FILE_VERSION);
404        serializer.attribute(ns, "suite", mSuiteName);
405
406        mResults.serialize(serializer);
407        // TODO: not sure why, but the serializer doesn't like this statement
408        //serializer.endTag(ns, RESULT_TAG);
409    }
410
411    private File getResultFile(File reportDir) {
412        return new File(reportDir, TEST_RESULT_FILE_NAME);
413    }
414
415    /**
416     * Creates the output stream to use for test results. Exposed for mocking.
417     */
418    OutputStream createOutputResultStream(File reportFile) throws IOException {
419        logResult("Created xml report file at file://%s", reportFile.getAbsolutePath());
420        return new FileOutputStream(reportFile);
421    }
422
423    /**
424     * Copy the xml formatting files stored in this jar to the results directory
425     *
426     * @param resultsDir
427     */
428    private void copyFormattingFiles(File resultsDir) {
429        for (String resultFileName : CTS_RESULT_RESOURCES) {
430            InputStream configStream = getClass().getResourceAsStream(String.format("/report/%s",
431                    resultFileName));
432            if (configStream != null) {
433                File resultFile = new File(resultsDir, resultFileName);
434                try {
435                    FileUtil.writeToFile(configStream, resultFile);
436                } catch (IOException e) {
437                    Log.w(LOG_TAG, String.format("Failed to write %s to file", resultFileName));
438                }
439            } else {
440                Log.w(LOG_TAG, String.format("Failed to load %s from jar", resultFileName));
441            }
442        }
443    }
444
445    /**
446     * Zip the contents of the given results directory.
447     *
448     * @param resultsDir
449     */
450    private void zipResults(File resultsDir) {
451        try {
452            // create a file in parent directory, with same name as resultsDir
453            File zipResultFile = new File(resultsDir.getParent(), String.format("%s.zip",
454                    resultsDir.getName()));
455            FileUtil.createZip(resultsDir, zipResultFile);
456        } catch (IOException e) {
457            Log.w(LOG_TAG, String.format("Failed to create zip for %s", resultsDir.getName()));
458        }
459    }
460
461    /**
462     * Get a String version of the current time.
463     * <p/>
464     * Exposed so unit tests can mock.
465     */
466    String getTimestamp() {
467        return TimeUtil.getTimestamp();
468    }
469
470    /**
471     * {@inheritDoc}
472     */
473    @Override
474    public void testRunFailed(String errorMessage) {
475        // ignore
476    }
477
478    /**
479     * {@inheritDoc}
480     */
481    @Override
482    public void testRunStopped(long elapsedTime) {
483        // ignore
484    }
485
486    /**
487     * {@inheritDoc}
488     */
489    @Override
490    public void invocationFailed(Throwable cause) {
491        // ignore
492    }
493
494    /**
495     * {@inheritDoc}
496     */
497    @Override
498    public TestSummary getSummary() {
499        return null;
500    }
501}
502