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.cts.tradefed.util.CtsHostStore; 23 import com.android.ddmlib.Log; 24 import com.android.ddmlib.Log.LogLevel; 25 import com.android.ddmlib.testrunner.TestIdentifier; 26 import com.android.tradefed.build.IBuildInfo; 27 import com.android.tradefed.build.IFolderBuildInfo; 28 import com.android.tradefed.config.Option; 29 import com.android.tradefed.log.LogUtil.CLog; 30 import com.android.tradefed.result.ITestInvocationListener; 31 import com.android.tradefed.result.InputStreamSource; 32 import com.android.tradefed.result.LogDataType; 33 import com.android.tradefed.result.LogFileSaver; 34 import com.android.tradefed.result.TestSummary; 35 import com.android.tradefed.util.FileUtil; 36 import com.android.tradefed.util.StreamUtil; 37 38 import org.kxml2.io.KXmlSerializer; 39 40 import java.io.File; 41 import java.io.FileNotFoundException; 42 import java.io.FileOutputStream; 43 import java.io.IOException; 44 import java.io.InputStream; 45 import java.io.OutputStream; 46 import java.util.Map; 47 import java.util.regex.Matcher; 48 import java.util.regex.Pattern; 49 50 /** 51 * Writes results to an XML files in the CTS format. 52 * <p/> 53 * Collects all test info in memory, then dumps to file when invocation is complete. 54 * <p/> 55 * Outputs xml in format governed by the cts_result.xsd 56 */ 57 public class CtsXmlResultReporter implements ITestInvocationListener { 58 private static final String LOG_TAG = "CtsXmlResultReporter"; 59 60 static final String TEST_RESULT_FILE_NAME = "testResult.xml"; 61 private static final String CTS_RESULT_FILE_VERSION = "4.4"; 62 private static final String[] CTS_RESULT_RESOURCES = {"cts_result.xsl", "cts_result.css", 63 "logo.gif", "newrule-green.png"}; 64 65 /** the XML namespace */ 66 static final String ns = null; 67 68 static final String RESULT_TAG = "TestResult"; 69 static final String PLAN_ATTR = "testPlan"; 70 static final String STARTTIME_ATTR = "starttime"; 71 72 private static final String REPORT_DIR_NAME = "output-file-path"; 73 @Option(name=REPORT_DIR_NAME, description="root file system path to directory to store xml " + 74 "test results and associated logs. If not specified, results will be stored at " + 75 "<cts root>/repository/results") 76 protected File mReportDir = null; 77 78 // listen in on the plan option provided to CtsTest 79 @Option(name = CtsTest.PLAN_OPTION, description = "the test plan to run.") 80 private String mPlanName = "NA"; 81 82 // listen in on the continue-session option provided to CtsTest 83 @Option(name = CtsTest.CONTINUE_OPTION, description = "the test result session to continue.") 84 private Integer mContinueSessionId = null; 85 86 @Option(name = "quiet-output", description = "Mute display of test results.") 87 private boolean mQuietOutput = false; 88 89 @Option(name = "result-server", description = "Server to publish test results.") 90 private String mResultServer; 91 92 protected IBuildInfo mBuildInfo; 93 private String mStartTime; 94 private String mDeviceSerial; 95 private TestResults mResults = new TestResults(); 96 private TestPackageResult mCurrentPkgResult = null; 97 private boolean mIsDeviceInfoRun = false; 98 private ResultReporter mReporter; 99 private File mLogDir; 100 private String mSuiteName; 101 102 private static final Pattern mCtsLogPattern = Pattern.compile("(.*)\\+\\+\\+\\+(.*)"); 103 104 public void setReportDir(File reportDir) { 105 mReportDir = reportDir; 106 } 107 108 /** 109 * {@inheritDoc} 110 */ 111 @Override 112 public void invocationStarted(IBuildInfo buildInfo) { 113 mBuildInfo = buildInfo; 114 if (!(buildInfo instanceof IFolderBuildInfo)) { 115 throw new IllegalArgumentException("build info is not a IFolderBuildInfo"); 116 } 117 IFolderBuildInfo ctsBuild = (IFolderBuildInfo)buildInfo; 118 CtsBuildHelper ctsBuildHelper = getBuildHelper(ctsBuild); 119 mDeviceSerial = buildInfo.getDeviceSerial() == null ? "unknown_device" : 120 buildInfo.getDeviceSerial(); 121 if (mContinueSessionId != null) { 122 CLog.d("Continuing session %d", mContinueSessionId); 123 // reuse existing directory 124 TestResultRepo resultRepo = new TestResultRepo(ctsBuildHelper.getResultsDir()); 125 mResults = resultRepo.getResult(mContinueSessionId); 126 if (mResults == null) { 127 throw new IllegalArgumentException(String.format("Could not find session %d", 128 mContinueSessionId)); 129 } 130 mPlanName = resultRepo.getSummaries().get(mContinueSessionId).getTestPlan(); 131 mStartTime = resultRepo.getSummaries().get(mContinueSessionId).getStartTime(); 132 mReportDir = resultRepo.getReportDir(mContinueSessionId); 133 } else { 134 if (mReportDir == null) { 135 mReportDir = ctsBuildHelper.getResultsDir(); 136 } 137 mReportDir = createUniqueReportDir(mReportDir); 138 139 mStartTime = getTimestamp(); 140 logResult("Created result dir %s", mReportDir.getName()); 141 } 142 mSuiteName = ctsBuildHelper.getSuiteName(); 143 mReporter = new ResultReporter(mResultServer, mSuiteName); 144 145 // TODO: allow customization of log dir 146 // create a unique directory for saving logs, with same name as result dir 147 File rootLogDir = getBuildHelper(ctsBuild).getLogsDir(); 148 mLogDir = new File(rootLogDir, mReportDir.getName()); 149 mLogDir.mkdirs(); 150 } 151 152 /** 153 * Create a unique directory for saving results. 154 * <p/> 155 * Currently using legacy CTS host convention of timestamp directory names. In case of 156 * collisions, will use {@link FileUtil} to generate unique file name. 157 * <p/> 158 * TODO: in future, consider using LogFileSaver to create build-specific directories 159 * 160 * @param parentDir the parent folder to create dir in 161 * @return the created directory 162 */ 163 private static synchronized File createUniqueReportDir(File parentDir) { 164 // TODO: in future, consider using LogFileSaver to create build-specific directories 165 166 File reportDir = new File(parentDir, TimeUtil.getResultTimestamp()); 167 if (reportDir.exists()) { 168 // directory with this timestamp exists already! Choose a unique, although uglier, name 169 try { 170 reportDir = FileUtil.createTempDir(TimeUtil.getResultTimestamp() + "_", parentDir); 171 } catch (IOException e) { 172 CLog.e(e); 173 CLog.e("Failed to create result directory %s", reportDir.getAbsolutePath()); 174 } 175 } else { 176 if (!reportDir.mkdirs()) { 177 // TODO: consider throwing an exception 178 CLog.e("mkdirs failed when attempting to create result directory %s", 179 reportDir.getAbsolutePath()); 180 } 181 } 182 return reportDir; 183 } 184 185 /** 186 * Helper method to retrieve the {@link CtsBuildHelper}. 187 * @param ctsBuild 188 */ 189 CtsBuildHelper getBuildHelper(IFolderBuildInfo ctsBuild) { 190 CtsBuildHelper buildHelper = new CtsBuildHelper(ctsBuild.getRootDir()); 191 try { 192 buildHelper.validateStructure(); 193 } catch (FileNotFoundException e) { 194 // just log an error - it might be expected if we failed to retrieve a build 195 CLog.e("Invalid CTS build %s", ctsBuild.getRootDir()); 196 } 197 return buildHelper; 198 } 199 200 /** 201 * {@inheritDoc} 202 */ 203 @Override 204 public void testLog(String dataName, LogDataType dataType, InputStreamSource dataStream) { 205 try { 206 File logFile = getLogFileSaver().saveAndZipLogData(dataName, dataType, 207 dataStream.createInputStream()); 208 logResult(String.format("Saved log %s", logFile.getName())); 209 } catch (IOException e) { 210 CLog.e("Failed to write log for %s", dataName); 211 } 212 } 213 214 /** 215 * Return the {@link LogFileSaver} to use. 216 * <p/> 217 * Exposed for unit testing. 218 */ 219 LogFileSaver getLogFileSaver() { 220 return new LogFileSaver(mLogDir); 221 } 222 223 /** 224 * {@inheritDoc} 225 */ 226 @Override 227 public void testRunStarted(String name, int numTests) { 228 if (mCurrentPkgResult != null && !name.equals(mCurrentPkgResult.getAppPackageName())) { 229 // display results from previous run 230 logCompleteRun(mCurrentPkgResult); 231 } 232 mIsDeviceInfoRun = name.equals(DeviceInfoCollector.APP_PACKAGE_NAME); 233 if (mIsDeviceInfoRun) { 234 logResult("Collecting device info"); 235 } else { 236 if (mCurrentPkgResult == null || !name.equals(mCurrentPkgResult.getAppPackageName())) { 237 logResult("-----------------------------------------"); 238 logResult("Test package %s started", name); 239 logResult("-----------------------------------------"); 240 } 241 mCurrentPkgResult = mResults.getOrCreatePackage(name); 242 } 243 244 } 245 246 /** 247 * {@inheritDoc} 248 */ 249 @Override 250 public void testStarted(TestIdentifier test) { 251 mCurrentPkgResult.insertTest(test); 252 } 253 254 /** 255 * {@inheritDoc} 256 */ 257 @Override 258 public void testFailed(TestFailure status, TestIdentifier test, String trace) { 259 mCurrentPkgResult.reportTestFailure(test, CtsTestStatus.FAIL, trace); 260 } 261 262 /** 263 * {@inheritDoc} 264 */ 265 @Override 266 public void testEnded(TestIdentifier test, Map<String, String> testMetrics) { 267 collectCtsResults(test, testMetrics); 268 mCurrentPkgResult.reportTestEnded(test); 269 Test result = mCurrentPkgResult.findTest(test); 270 String stack = result.getStackTrace() == null ? "" : "\n" + result.getStackTrace(); 271 logResult("%s#%s %s %s", test.getClassName(), test.getTestName(), result.getResult(), 272 stack); 273 } 274 275 /** 276 * Collect Cts results for both device and host tests to the package result. 277 * @param test test ran 278 * @param testMetrics test metrics which can contain performance result for device tests 279 */ 280 private void collectCtsResults(TestIdentifier test, Map<String, String> testMetrics) { 281 // device test can have performance results in testMetrics 282 String perfResult = CtsReportUtil.getCtsResultFromMetrics(testMetrics); 283 // host test should be checked in CtsHostStore. 284 if (perfResult == null) { 285 perfResult = CtsHostStore.removeCtsResult(mDeviceSerial, test.toString()); 286 } 287 if (perfResult != null) { 288 // CTS result is passed in Summary++++Details format. 289 // Extract Summary and Details, and pass them. 290 Matcher m = mCtsLogPattern.matcher(perfResult); 291 if (m.find()) { 292 mCurrentPkgResult.reportPerformanceResult(test, CtsTestStatus.PASS, m.group(1), 293 m.group(2)); 294 } else { 295 logResult("CTS Result unrecognizable:" + perfResult); 296 } 297 } 298 } 299 300 /** 301 * {@inheritDoc} 302 */ 303 @Override 304 public void testRunEnded(long elapsedTime, Map<String, String> runMetrics) { 305 if (mIsDeviceInfoRun) { 306 mResults.populateDeviceInfoMetrics(runMetrics); 307 } else { 308 mCurrentPkgResult.populateMetrics(runMetrics); 309 } 310 } 311 312 /** 313 * {@inheritDoc} 314 */ 315 @Override 316 public void invocationEnded(long elapsedTime) { 317 // display the results of the last completed run 318 if (mCurrentPkgResult != null) { 319 logCompleteRun(mCurrentPkgResult); 320 } 321 if (mReportDir == null || mStartTime == null) { 322 // invocationStarted must have failed, abort 323 CLog.w("Unable to create XML report"); 324 return; 325 } 326 327 File reportFile = getResultFile(mReportDir); 328 createXmlResult(reportFile, mStartTime, elapsedTime); 329 copyFormattingFiles(mReportDir); 330 zipResults(mReportDir); 331 332 try { 333 mReporter.reportResult(reportFile); 334 } catch (IOException e) { 335 CLog.e(e); 336 } 337 } 338 339 private void logResult(String format, Object... args) { 340 if (mQuietOutput) { 341 CLog.i(format, args); 342 } else { 343 Log.logAndDisplay(LogLevel.INFO, mDeviceSerial, String.format(format, args)); 344 } 345 } 346 347 private void logCompleteRun(TestPackageResult pkgResult) { 348 if (pkgResult.getAppPackageName().equals(DeviceInfoCollector.APP_PACKAGE_NAME)) { 349 logResult("Device info collection complete"); 350 return; 351 } 352 logResult("%s package complete: Passed %d, Failed %d, Not Executed %d", 353 pkgResult.getAppPackageName(), pkgResult.countTests(CtsTestStatus.PASS), 354 pkgResult.countTests(CtsTestStatus.FAIL), 355 pkgResult.countTests(CtsTestStatus.NOT_EXECUTED)); 356 } 357 358 /** 359 * Creates a report file and populates it with the report data from the completed tests. 360 */ 361 private void createXmlResult(File reportFile, String startTimestamp, long elapsedTime) { 362 String endTime = getTimestamp(); 363 OutputStream stream = null; 364 try { 365 stream = createOutputResultStream(reportFile); 366 KXmlSerializer serializer = new KXmlSerializer(); 367 serializer.setOutput(stream, "UTF-8"); 368 serializer.startDocument("UTF-8", false); 369 serializer.setFeature( 370 "http://xmlpull.org/v1/doc/features.html#indent-output", true); 371 serializer.processingInstruction("xml-stylesheet type=\"text/xsl\" " + 372 "href=\"cts_result.xsl\""); 373 serializeResultsDoc(serializer, startTimestamp, endTime); 374 serializer.endDocument(); 375 String msg = String.format("XML test result file generated at %s. Passed %d, " + 376 "Failed %d, Not Executed %d", mReportDir.getName(), 377 mResults.countTests(CtsTestStatus.PASS), 378 mResults.countTests(CtsTestStatus.FAIL), 379 mResults.countTests(CtsTestStatus.NOT_EXECUTED)); 380 logResult(msg); 381 logResult("Time: %s", TimeUtil.formatElapsedTime(elapsedTime)); 382 } catch (IOException e) { 383 Log.e(LOG_TAG, "Failed to generate report data"); 384 } finally { 385 StreamUtil.closeStream(stream); 386 } 387 } 388 389 /** 390 * Output the results XML. 391 * 392 * @param serializer the {@link KXmlSerializer} to use 393 * @param startTime the user-friendly starting time of the test invocation 394 * @param endTime the user-friendly ending time of the test invocation 395 * @throws IOException 396 */ 397 private void serializeResultsDoc(KXmlSerializer serializer, String startTime, String endTime) 398 throws IOException { 399 serializer.startTag(ns, RESULT_TAG); 400 serializer.attribute(ns, PLAN_ATTR, mPlanName); 401 serializer.attribute(ns, STARTTIME_ATTR, startTime); 402 serializer.attribute(ns, "endtime", endTime); 403 serializer.attribute(ns, "version", CTS_RESULT_FILE_VERSION); 404 serializer.attribute(ns, "suite", mSuiteName); 405 406 mResults.serialize(serializer); 407 // TODO: not sure why, but the serializer doesn't like this statement 408 //serializer.endTag(ns, RESULT_TAG); 409 } 410 411 private File getResultFile(File reportDir) { 412 return new File(reportDir, TEST_RESULT_FILE_NAME); 413 } 414 415 /** 416 * Creates the output stream to use for test results. Exposed for mocking. 417 */ 418 OutputStream createOutputResultStream(File reportFile) throws IOException { 419 logResult("Created xml report file at file://%s", reportFile.getAbsolutePath()); 420 return new FileOutputStream(reportFile); 421 } 422 423 /** 424 * Copy the xml formatting files stored in this jar to the results directory 425 * 426 * @param resultsDir 427 */ 428 private void copyFormattingFiles(File resultsDir) { 429 for (String resultFileName : CTS_RESULT_RESOURCES) { 430 InputStream configStream = getClass().getResourceAsStream(String.format("/report/%s", 431 resultFileName)); 432 if (configStream != null) { 433 File resultFile = new File(resultsDir, resultFileName); 434 try { 435 FileUtil.writeToFile(configStream, resultFile); 436 } catch (IOException e) { 437 Log.w(LOG_TAG, String.format("Failed to write %s to file", resultFileName)); 438 } 439 } else { 440 Log.w(LOG_TAG, String.format("Failed to load %s from jar", resultFileName)); 441 } 442 } 443 } 444 445 /** 446 * Zip the contents of the given results directory. 447 * 448 * @param resultsDir 449 */ 450 private void zipResults(File resultsDir) { 451 try { 452 // create a file in parent directory, with same name as resultsDir 453 File zipResultFile = new File(resultsDir.getParent(), String.format("%s.zip", 454 resultsDir.getName())); 455 FileUtil.createZip(resultsDir, zipResultFile); 456 } catch (IOException e) { 457 Log.w(LOG_TAG, String.format("Failed to create zip for %s", resultsDir.getName())); 458 } 459 } 460 461 /** 462 * Get a String version of the current time. 463 * <p/> 464 * Exposed so unit tests can mock. 465 */ 466 String getTimestamp() { 467 return TimeUtil.getTimestamp(); 468 } 469 470 /** 471 * {@inheritDoc} 472 */ 473 @Override 474 public void testRunFailed(String errorMessage) { 475 // ignore 476 } 477 478 /** 479 * {@inheritDoc} 480 */ 481 @Override 482 public void testRunStopped(long elapsedTime) { 483 // ignore 484 } 485 486 /** 487 * {@inheritDoc} 488 */ 489 @Override 490 public void invocationFailed(Throwable cause) { 491 // ignore 492 } 493 494 /** 495 * {@inheritDoc} 496 */ 497 @Override 498 public TestSummary getSummary() { 499 return null; 500 } 501 } 502