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