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