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