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