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