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