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