CtsXmlResultReporter.java revision 82d9daf8b1800ef1c7897f2cd3167b6b1920b9fa
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.device.DeviceInfoCollector; 20import com.android.cts.tradefed.targetsetup.CtsBuildHelper; 21import com.android.ddmlib.Log; 22import com.android.ddmlib.Log.LogLevel; 23import com.android.ddmlib.testrunner.TestIdentifier; 24import com.android.tradefed.config.Option; 25import com.android.tradefed.result.CollectingTestListener; 26import com.android.tradefed.result.LogDataType; 27import com.android.tradefed.result.TestResult; 28import com.android.tradefed.result.TestRunResult; 29import com.android.tradefed.result.TestResult.TestStatus; 30import com.android.tradefed.targetsetup.IBuildInfo; 31import com.android.tradefed.targetsetup.IFolderBuildInfo; 32import com.android.tradefed.util.FileUtil; 33 34import org.kxml2.io.KXmlSerializer; 35 36import java.io.File; 37import java.io.FileNotFoundException; 38import java.io.FileOutputStream; 39import java.io.IOException; 40import java.io.InputStream; 41import java.io.OutputStream; 42import java.net.InetAddress; 43import java.net.UnknownHostException; 44import java.text.SimpleDateFormat; 45import java.util.Date; 46import java.util.HashMap; 47import java.util.LinkedHashMap; 48import java.util.Map; 49import java.util.concurrent.TimeUnit; 50 51/** 52 * Writes results to an XML files in the CTS format. 53 * <p/> 54 * Collects all test info in memory, then dumps to file when invocation is complete. 55 * <p/> 56 * Outputs xml in format governed by the cts_result.xsd 57 */ 58public class CtsXmlResultReporter extends CollectingTestListener { 59 60 private static final String LOG_TAG = "CtsXmlResultReporter"; 61 62 private static final String TEST_RESULT_FILE_NAME = "testResult.xml"; 63 private static final String CTS_RESULT_FILE_VERSION = "2.0"; 64 private static final String CTS_VERSION = "99"; 65 66 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 private static final String ns = null; 72 73 private static final String REPORT_DIR_NAME = "output-file-path"; 74 @Option(name=REPORT_DIR_NAME, description="root file system path to directory to store xml " + 75 "test results and associated logs. If not specified, results will be stored at " + 76 "<cts root>/repository/results") 77 protected File mReportDir = null; 78 79 protected IBuildInfo mBuildInfo; 80 81 private String mStartTime; 82 83 public void setReportDir(File reportDir) { 84 mReportDir = reportDir; 85 } 86 87 /** 88 * {@inheritDoc} 89 */ 90 @Override 91 public void invocationStarted(IBuildInfo buildInfo) { 92 super.invocationStarted(buildInfo); 93 if (mReportDir == null) { 94 if (!(buildInfo instanceof IFolderBuildInfo)) { 95 throw new IllegalArgumentException("build info is not a IFolderBuildInfo"); 96 } 97 IFolderBuildInfo ctsBuild = (IFolderBuildInfo)buildInfo; 98 try { 99 CtsBuildHelper buildHelper = new CtsBuildHelper(ctsBuild.getRootDir()); 100 mReportDir = buildHelper.getResultsDir(); 101 102 } catch (FileNotFoundException e) { 103 throw new IllegalArgumentException("unrecognized cts structure", e); 104 } 105 } 106 // create a unique directory for saving results, using old cts host convention 107 // TODO: in future, consider using LogFileSaver to create build-specific directories 108 mReportDir = new File(mReportDir, getResultTimestamp()); 109 mReportDir.mkdirs(); 110 mStartTime = getTimestamp(); 111 } 112 113 /** 114 * {@inheritDoc} 115 */ 116 @Override 117 public void testLog(String dataName, LogDataType dataType, InputStream dataStream) { 118 // TODO: implement this 119 } 120 121 /** 122 * {@inheritDoc} 123 */ 124 @Override 125 public void testFailed(TestFailure status, TestIdentifier test, String trace) { 126 super.testFailed(status, test, trace); 127 Log.i(LOG_TAG, String.format("Test %s#%s: %s\n%s", test.getClassName(), test.getTestName(), 128 status.toString(), trace)); 129 } 130 131 /** 132 * {@inheritDoc} 133 */ 134 @Override 135 public void testRunEnded(long elapsedTime, Map<String, String> runMetrics) { 136 super.testRunEnded(elapsedTime, runMetrics); 137 Log.i(LOG_TAG, String.format("Test run %s complete. Tests passed %d, failed %d, error %d", 138 getCurrentRunResults().getName(), getCurrentRunResults().getNumPassedTests(), 139 getCurrentRunResults().getNumFailedTests(), 140 getCurrentRunResults().getNumErrorTests())); 141 } 142 143 /** 144 * {@inheritDoc} 145 */ 146 @Override 147 public void invocationEnded(long elapsedTime) { 148 super.invocationEnded(elapsedTime); 149 createXmlResult(mReportDir, mStartTime, elapsedTime); 150 copyFormattingFiles(mReportDir); 151 zipResults(mReportDir); 152 } 153 154 /** 155 * Creates a report file and populates it with the report data from the completed tests. 156 */ 157 private void createXmlResult(File reportDir, String startTimestamp, long elapsedTime) { 158 String endTime = getTimestamp(); 159 160 OutputStream stream = null; 161 try { 162 stream = createOutputResultStream(reportDir); 163 KXmlSerializer serializer = new KXmlSerializer(); 164 serializer.setOutput(stream, "UTF-8"); 165 serializer.startDocument("UTF-8", false); 166 serializer.setFeature( 167 "http://xmlpull.org/v1/doc/features.html#indent-output", true); 168 serializer.processingInstruction("xml-stylesheet type=\"text/xsl\" " + 169 "href=\"cts_result.xsl\""); 170 serializeResultsDoc(serializer, startTimestamp, endTime); 171 serializer.endDocument(); 172 // TODO: output not executed timeout omitted counts 173 String msg = String.format("XML test result file generated at %s. Total tests %d, " + 174 "Failed %d, Error %d", reportDir.getAbsolutePath(), getNumTotalTests(), 175 getNumFailedTests(), getNumErrorTests()); 176 Log.logAndDisplay(LogLevel.INFO, LOG_TAG, msg); 177 Log.logAndDisplay(LogLevel.INFO, LOG_TAG, String.format("Time: %s", 178 formatElapsedTime(elapsedTime))); 179 } catch (IOException e) { 180 Log.e(LOG_TAG, "Failed to generate report data"); 181 } finally { 182 if (stream != null) { 183 try { 184 stream.close(); 185 } catch (IOException ignored) { 186 } 187 } 188 } 189 } 190 191 /** 192 * Output the results XML. 193 * 194 * @param serializer the {@link KXmlSerializer} to use 195 * @param startTime the user-friendly starting time of the test invocation 196 * @param endTime the user-friendly ending time of the test invocation 197 * @throws IOException 198 */ 199 private void serializeResultsDoc(KXmlSerializer serializer, String startTime, String endTime) 200 throws IOException { 201 serializer.startTag(ns, "TestResult"); 202 // TODO: output test plan and profile values 203 serializer.attribute(ns, "testPlan", "unknown"); 204 serializer.attribute(ns, "profile", "unknown"); 205 serializer.attribute(ns, "starttime", startTime); 206 serializer.attribute(ns, "endtime", endTime); 207 serializer.attribute(ns, "version", CTS_RESULT_FILE_VERSION); 208 209 serializeDeviceInfo(serializer); 210 serializeHostInfo(serializer); 211 serializeTestSummary(serializer); 212 serializeTestResults(serializer); 213 } 214 215 /** 216 * Output the device info XML. 217 * 218 * @param serializer 219 */ 220 private void serializeDeviceInfo(KXmlSerializer serializer) throws IOException { 221 serializer.startTag(ns, "DeviceInfo"); 222 223 TestRunResult deviceInfoResult = findRunResult(DeviceInfoCollector.APP_PACKAGE_NAME); 224 if (deviceInfoResult == null) { 225 Log.w(LOG_TAG, String.format("Could not find device info run %s", 226 DeviceInfoCollector.APP_PACKAGE_NAME)); 227 return; 228 } 229 // Extract metrics that need extra handling, and then dump the remainder into BuildInfo 230 Map<String, String> metricsCopy = new HashMap<String, String>( 231 deviceInfoResult.getRunMetrics()); 232 serializer.startTag(ns, "Screen"); 233 String screenWidth = metricsCopy.remove(DeviceInfoCollector.SCREEN_WIDTH); 234 String screenHeight = metricsCopy.remove(DeviceInfoCollector.SCREEN_HEIGHT); 235 serializer.attribute(ns, "resolution", String.format("%sx%s", screenWidth, screenHeight)); 236 serializer.endTag(ns, "Screen"); 237 238 serializer.startTag(ns, "PhoneSubInfo"); 239 serializer.attribute(ns, "subscriberId", metricsCopy.remove( 240 DeviceInfoCollector.PHONE_NUMBER)); 241 serializer.endTag(ns, "PhoneSubInfo"); 242 243 String featureData = metricsCopy.remove(DeviceInfoCollector.FEATURES); 244 String processData = metricsCopy.remove(DeviceInfoCollector.PROCESSES); 245 246 // dump the remaining metrics without translation 247 serializer.startTag(ns, "BuildInfo"); 248 for (Map.Entry<String, String> metricEntry : metricsCopy.entrySet()) { 249 serializer.attribute(ns, metricEntry.getKey(), metricEntry.getValue()); 250 } 251 serializer.endTag(ns, "BuildInfo"); 252 253 serializeFeatureInfo(serializer, featureData); 254 serializeProcessInfo(serializer, processData); 255 256 serializer.endTag(ns, "DeviceInfo"); 257 } 258 259 /** 260 * Prints XML indicating what features are supported by the device. It parses a string from the 261 * featureData argument that is in the form of "feature1:true;feature2:false;featuer3;true;" 262 * with a trailing semi-colon. 263 * 264 * <pre> 265 * <FeatureInfo> 266 * <Feature name="android.name.of.feature" available="true" /> 267 * ... 268 * </FeatureInfo> 269 * </pre> 270 * 271 * @param serializer used to create XML 272 * @param featureData raw unparsed feature data 273 */ 274 private void serializeFeatureInfo(KXmlSerializer serializer, String featureData) throws IOException { 275 serializer.startTag(ns, "FeatureInfo"); 276 277 if (featureData == null) { 278 featureData = ""; 279 } 280 281 String[] featurePairs = featureData.split(";"); 282 for (String featurePair : featurePairs) { 283 String[] nameTypeAvailability = featurePair.split(":"); 284 if (nameTypeAvailability.length >= 3) { 285 serializer.startTag(ns, "Feature"); 286 serializer.attribute(ns, "name", nameTypeAvailability[0]); 287 serializer.attribute(ns, "type", nameTypeAvailability[1]); 288 serializer.attribute(ns, "available", nameTypeAvailability[2]); 289 serializer.endTag(ns, "Feature"); 290 } 291 } 292 serializer.endTag(ns, "FeatureInfo"); 293 } 294 295 /** 296 * Prints XML data indicating what particular processes of interest were running on the device. 297 * It parses a string from the rootProcesses argument that is in the form of 298 * "processName1;processName2;..." with a trailing semi-colon. 299 * 300 * <pre> 301 * <ProcessInfo> 302 * <Process name="long_cat_viewer" uid="0" /> 303 * ... 304 * </ProcessInfo> 305 * </pre> 306 * 307 * @param document 308 * @param parentNode 309 * @param deviceInfo 310 */ 311 private void serializeProcessInfo(KXmlSerializer serializer, String rootProcesses) 312 throws IOException { 313 serializer.startTag(ns, "ProcessInfo"); 314 315 if (rootProcesses == null) { 316 rootProcesses = ""; 317 } 318 319 String[] processNames = rootProcesses.split(";"); 320 for (String processName : processNames) { 321 processName = processName.trim(); 322 if (processName.length() > 0) { 323 serializer.startTag(ns, "Process"); 324 serializer.attribute(ns, "name", processName); 325 serializer.attribute(ns, "uid", "0"); 326 serializer.endTag(ns, "Process"); 327 } 328 } 329 serializer.endTag(ns, "ProcessInfo"); 330 } 331 332 /** 333 * Finds the {@link TestRunResult} with the given name. 334 * 335 * @param runName 336 * @return the {@link TestRunResult} 337 */ 338 private TestRunResult findRunResult(String runName) { 339 for (TestRunResult runResult : getRunResults()) { 340 if (runResult.getName().equals(runName)) { 341 return runResult; 342 } 343 } 344 return null; 345 } 346 347 /** 348 * Output the host info XML. 349 * 350 * @param serializer 351 */ 352 private void serializeHostInfo(KXmlSerializer serializer) throws IOException { 353 serializer.startTag(ns, "HostInfo"); 354 355 String hostName = ""; 356 try { 357 hostName = InetAddress.getLocalHost().getHostName(); 358 } catch (UnknownHostException ignored) {} 359 serializer.attribute(ns, "name", hostName); 360 361 serializer.startTag(ns, "Os"); 362 serializer.attribute(ns, "name", System.getProperty("os.name")); 363 serializer.attribute(ns, "version", System.getProperty("os.version")); 364 serializer.attribute(ns, "arch", System.getProperty("os.arch")); 365 serializer.endTag(ns, "Os"); 366 367 serializer.startTag(ns, "Java"); 368 serializer.attribute(ns, "name", System.getProperty("java.vendor")); 369 serializer.attribute(ns, "version", System.getProperty("java.version")); 370 serializer.endTag(ns, "Java"); 371 372 serializer.startTag(ns, "Cts"); 373 serializer.attribute(ns, "version", CTS_VERSION); 374 // TODO: consider outputting tradefed options here 375 serializer.endTag(ns, "Cts"); 376 377 serializer.endTag(ns, "HostInfo"); 378 } 379 380 /** 381 * Output the test summary XML containing summary totals for all tests. 382 * 383 * @param serializer 384 * @throws IOException 385 */ 386 private void serializeTestSummary(KXmlSerializer serializer) throws IOException { 387 serializer.startTag(ns, "Summary"); 388 serializer.attribute(ns, "failed", Integer.toString(getNumErrorTests() + 389 getNumFailedTests())); 390 // TODO: output notExecuted, timeout, and omitted count 391 serializer.attribute(ns, "notExecuted", "0"); 392 serializer.attribute(ns, "timeout", "0"); 393 serializer.attribute(ns, "omitted", "0"); 394 serializer.attribute(ns, "pass", Integer.toString(getNumPassedTests())); 395 serializer.attribute(ns, "total", Integer.toString(getNumTotalTests())); 396 serializer.endTag(ns, "Summary"); 397 } 398 399 /** 400 * Output the detailed test results XML. 401 * 402 * @param serializer 403 * @throws IOException 404 */ 405 private void serializeTestResults(KXmlSerializer serializer) throws IOException { 406 for (TestRunResult runResult : getRunResults()) { 407 serializeTestRunResult(serializer, runResult); 408 } 409 } 410 411 /** 412 * Output the XML for one test run aka test package. 413 * 414 * @param serializer 415 * @param runResult the {@link TestRunResult} 416 * @throws IOException 417 */ 418 private void serializeTestRunResult(KXmlSerializer serializer, TestRunResult runResult) 419 throws IOException { 420 if (runResult.getName().equals(DeviceInfoCollector.APP_PACKAGE_NAME)) { 421 // ignore run results for the info collecting packages 422 return; 423 } 424 serializer.startTag(ns, "TestPackage"); 425 serializer.attribute(ns, "name", runResult.getName()); 426 serializer.attribute(ns, "runTime", formatElapsedTime(runResult.getElapsedTime())); 427 // TODO: generate digest 428 serializer.attribute(ns, "digest", ""); 429 serializer.attribute(ns, "failed", Integer.toString(runResult.getNumErrorTests() + 430 runResult.getNumFailedTests())); 431 // TODO: output notExecuted, timeout, and omitted count 432 serializer.attribute(ns, "notExecuted", "0"); 433 serializer.attribute(ns, "timeout", "0"); 434 serializer.attribute(ns, "omitted", "0"); 435 serializer.attribute(ns, "pass", Integer.toString(runResult.getNumPassedTests())); 436 serializer.attribute(ns, "total", Integer.toString(runResult.getNumTests())); 437 438 // the results XML needs to organize test's by class. Build a nested data structure that 439 // group's the results by class name 440 Map<String, Map<TestIdentifier, TestResult>> classResultsMap = buildClassNameMap( 441 runResult.getTestResults()); 442 443 for (Map.Entry<String, Map<TestIdentifier, TestResult>> resultsEntry : 444 classResultsMap.entrySet()) { 445 serializer.startTag(ns, "TestCase"); 446 serializer.attribute(ns, "name", resultsEntry.getKey()); 447 serializeTests(serializer, resultsEntry.getValue()); 448 serializer.endTag(ns, "TestCase"); 449 } 450 serializer.endTag(ns, "TestPackage"); 451 } 452 453 /** 454 * Organizes the test run results into a format organized by class name. 455 */ 456 private Map<String, Map<TestIdentifier, TestResult>> buildClassNameMap( 457 Map<TestIdentifier, TestResult> results) { 458 // use a linked hashmap to have predictable iteration order 459 Map<String, Map<TestIdentifier, TestResult>> classResultMap = 460 new LinkedHashMap<String, Map<TestIdentifier, TestResult>>(); 461 for (Map.Entry<TestIdentifier, TestResult> resultEntry : results.entrySet()) { 462 String className = resultEntry.getKey().getClassName(); 463 Map<TestIdentifier, TestResult> resultsForClass = classResultMap.get(className); 464 if (resultsForClass == null) { 465 resultsForClass = new LinkedHashMap<TestIdentifier, TestResult>(); 466 classResultMap.put(className, resultsForClass); 467 } 468 resultsForClass.put(resultEntry.getKey(), resultEntry.getValue()); 469 } 470 return classResultMap; 471 } 472 473 /** 474 * Output XML for given map of tests their results 475 * 476 * @param serializer 477 * @param results 478 * @throws IOException 479 */ 480 private void serializeTests(KXmlSerializer serializer, Map<TestIdentifier, TestResult> results) 481 throws IOException { 482 for (Map.Entry<TestIdentifier, TestResult> resultEntry : results.entrySet()) { 483 serializeTest(serializer, resultEntry.getKey(), resultEntry.getValue()); 484 } 485 } 486 487 /** 488 * Output the XML for given test and result. 489 * 490 * @param serializer 491 * @param testId 492 * @param result 493 * @throws IOException 494 */ 495 private void serializeTest(KXmlSerializer serializer, TestIdentifier testId, TestResult result) 496 throws IOException { 497 serializer.startTag(ns, "Test"); 498 serializer.attribute(ns, "name", testId.getTestName()); 499 serializer.attribute(ns, "result", convertStatus(result.getStatus())); 500 501 if (result.getStackTrace() != null) { 502 String sanitizedStack = sanitizeStackTrace(result.getStackTrace()); 503 serializer.startTag(ns, "FailedScene"); 504 serializer.attribute(ns, "message", getFailureMessageFromStackTrace(sanitizedStack)); 505 serializer.text(sanitizedStack); 506 serializer.endTag(ns, "FailedScene"); 507 } 508 serializer.endTag(ns, "Test"); 509 } 510 511 /** 512 * Convert a {@link TestStatus} to the result text to output in XML 513 * 514 * @param status the {@link TestStatus} 515 * @return 516 */ 517 private String convertStatus(TestStatus status) { 518 switch (status) { 519 case ERROR: 520 return "fail"; 521 case FAILURE: 522 return "fail"; 523 case PASSED: 524 return "pass"; 525 // TODO add notExecuted, omitted timeout 526 } 527 return "omitted"; 528 } 529 530 /** 531 * Strip out any invalid XML characters that might cause the report to be unviewable. 532 * http://www.w3.org/TR/REC-xml/#dt-character 533 */ 534 private static String sanitizeStackTrace(String trace) { 535 if (trace != null) { 536 return trace.replaceAll("[^\\u0009\\u000A\\u000D\\u0020-\\uD7FF\\uE000-\\uFFFD]", ""); 537 } else { 538 return null; 539 } 540 } 541 542 private static String getFailureMessageFromStackTrace(String stack) { 543 // This is probably too simplistic to work in all cases, but for now, just return first 544 // line of stack as failure message 545 int firstNewLine = stack.indexOf('\n'); 546 if (firstNewLine != -1) { 547 return stack.substring(0, firstNewLine); 548 } 549 return stack; 550 } 551 552 /** 553 * Return the current timestamp as a {@link String} suitable for displaying. 554 * <p/> 555 * Example: Fri Aug 20 15:13:03 PDT 2010 556 */ 557 String getTimestamp() { 558 SimpleDateFormat dateFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy"); 559 return dateFormat.format(new Date()); 560 } 561 562 /** 563 * Return the current timestamp in a compressed format, used to uniquely identify results. 564 * <p/> 565 * Example: 2010.08.16_11.42.12 566 */ 567 private String getResultTimestamp() { 568 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd_HH.mm.ss"); 569 return dateFormat.format(new Date()); 570 } 571 572 /** 573 * Return a prettified version of the given elapsed time 574 * @return 575 */ 576 private String formatElapsedTime(long elapsedTimeMs) { 577 long seconds = TimeUnit.MILLISECONDS.toSeconds(elapsedTimeMs) % 60; 578 long minutes = TimeUnit.MILLISECONDS.toMinutes(elapsedTimeMs) % 60; 579 long hours = TimeUnit.MILLISECONDS.toHours(elapsedTimeMs); 580 StringBuilder time = new StringBuilder(); 581 if (hours > 0) { 582 time.append(hours); 583 time.append("h "); 584 } 585 if (minutes > 0) { 586 time.append(minutes); 587 time.append("m "); 588 } 589 time.append(seconds); 590 time.append("s"); 591 592 return time.toString(); 593 } 594 595 /** 596 * Creates the output stream to use for test results. Exposed for mocking. 597 */ 598 OutputStream createOutputResultStream(File reportDir) throws IOException { 599 File reportFile = new File(reportDir, TEST_RESULT_FILE_NAME); 600 Log.i(LOG_TAG, String.format("Created xml report file at %s", 601 reportFile.getAbsolutePath())); 602 return new FileOutputStream(reportFile); 603 } 604 605 /** 606 * Copy the xml formatting files stored in this jar to the results directory 607 * 608 * @param resultsDir 609 */ 610 private void copyFormattingFiles(File resultsDir) { 611 for (String resultFileName : CTS_RESULT_RESOURCES) { 612 InputStream configStream = getClass().getResourceAsStream( 613 String.format("/result/%s", resultFileName)); 614 if (configStream != null) { 615 File resultFile = new File(resultsDir, resultFileName); 616 try { 617 FileUtil.writeToFile(configStream, resultFile); 618 } catch (IOException e) { 619 Log.w(LOG_TAG, String.format("Failed to write %s to file", resultFileName)); 620 } 621 } else { 622 Log.w(LOG_TAG, String.format("Failed to load %s from jar", resultFileName)); 623 } 624 } 625 } 626 627 /** 628 * Zip the contents of the given results directory. 629 * 630 * @param resultsDir 631 */ 632 private void zipResults(File resultsDir) { 633 // TODO: implement this 634 } 635} 636