CtsXmlResultReporter.java revision 98e9f2ce6c8d34b99242c237cdeb3cdb42b3f924
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 android.tests.getinfo.DeviceInfoConstants; 20 21import com.android.cts.tradefed.build.CtsBuildHelper; 22import com.android.cts.tradefed.build.CtsBuildProvider; 23import com.android.cts.tradefed.device.DeviceInfoCollector; 24import com.android.cts.tradefed.testtype.CtsTest; 25import com.android.ddmlib.Log; 26import com.android.ddmlib.Log.LogLevel; 27import com.android.ddmlib.testrunner.TestIdentifier; 28import com.android.tradefed.build.IBuildInfo; 29import com.android.tradefed.build.IFolderBuildInfo; 30import com.android.tradefed.config.Option; 31import com.android.tradefed.log.LogUtil.CLog; 32import com.android.tradefed.result.ITestInvocationListener; 33import com.android.tradefed.result.InputStreamSource; 34import com.android.tradefed.result.LogDataType; 35import com.android.tradefed.result.TestSummary; 36import com.android.tradefed.util.FileUtil; 37import com.android.tradefed.util.StreamUtil; 38 39import org.kxml2.io.KXmlSerializer; 40 41import java.io.File; 42import java.io.FileNotFoundException; 43import java.io.FileOutputStream; 44import java.io.IOException; 45import java.io.InputStream; 46import java.io.OutputStream; 47import java.net.InetAddress; 48import java.net.UnknownHostException; 49import java.util.HashMap; 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 implements ITestInvocationListener { 60 61 private static final String LOG_TAG = "CtsXmlResultReporter"; 62 63 static final String TEST_RESULT_FILE_NAME = "testResult.xml"; 64 private static final String CTS_RESULT_FILE_VERSION = "1.11"; 65 private static final String[] CTS_RESULT_RESOURCES = {"cts_result.xsl", "cts_result.css", 66 "logo.gif", "newrule-green.png"}; 67 68 /** the XML namespace */ 69 static final String ns = null; 70 71 // XML constants 72 static final String SUMMARY_TAG = "Summary"; 73 static final String PASS_ATTR = "pass"; 74 static final String TIMEOUT_ATTR = "timeout"; 75 static final String NOT_EXECUTED_ATTR = "notExecuted"; 76 static final String FAILED_ATTR = "failed"; 77 static final String RESULT_TAG = "TestResult"; 78 static final String PLAN_ATTR = "testPlan"; 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 = "quiet-output", description = "Mute display of test results.") 95 private boolean mQuietOutput = false; 96 97 protected IBuildInfo mBuildInfo; 98 private String mStartTime; 99 private String mDeviceSerial; 100 private TestResults mResults = new TestResults(); 101 private TestPackageResult mCurrentPkgResult = null; 102 103 public void setReportDir(File reportDir) { 104 mReportDir = reportDir; 105 } 106 107 /** 108 * {@inheritDoc} 109 */ 110 @Override 111 public void invocationStarted(IBuildInfo buildInfo) { 112 mBuildInfo = buildInfo; 113 if (!(buildInfo instanceof IFolderBuildInfo)) { 114 throw new IllegalArgumentException("build info is not a IFolderBuildInfo"); 115 } 116 IFolderBuildInfo ctsBuild = (IFolderBuildInfo)buildInfo; 117 mDeviceSerial = buildInfo.getDeviceSerial() == null ? "unknown_device" : 118 buildInfo.getDeviceSerial(); 119 if (mContinueSessionId != null) { 120 CLog.d("Continuing session %d", mContinueSessionId); 121 // reuse existing directory 122 TestResultRepo resultRepo = new TestResultRepo(getBuildHelper(ctsBuild).getResultsDir()); 123 mResults = resultRepo.getResult(mContinueSessionId); 124 if (mResults == null) { 125 throw new IllegalArgumentException(String.format("Could not find session %d", 126 mContinueSessionId)); 127 } 128 mPlanName = resultRepo.getSummaries().get(mContinueSessionId).getTestPlan(); 129 mStartTime = resultRepo.getSummaries().get(mContinueSessionId).getTimestamp(); 130 mReportDir = resultRepo.getReportDir(mContinueSessionId); 131 } else { 132 if (mReportDir == null) { 133 mReportDir = getBuildHelper(ctsBuild).getResultsDir(); 134 } 135 // create a unique directory for saving results, using old cts host convention 136 // TODO: in future, consider using LogFileSaver to create build-specific directories 137 mReportDir = new File(mReportDir, TimeUtil.getResultTimestamp()); 138 mReportDir.mkdirs(); 139 mStartTime = getTimestamp(); 140 logResult("Created result dir %s", mReportDir.getName()); 141 } 142 } 143 144 /** 145 * Helper method to retrieve the {@link CtsBuildHelper}. 146 * @param ctsBuild 147 */ 148 CtsBuildHelper getBuildHelper(IFolderBuildInfo ctsBuild) { 149 CtsBuildHelper buildHelper = new CtsBuildHelper(ctsBuild.getRootDir()); 150 try { 151 buildHelper.validateStructure(); 152 } catch (FileNotFoundException e) { 153 throw new IllegalArgumentException("Invalid CTS build", e); 154 } 155 return buildHelper; 156 } 157 158 /** 159 * {@inheritDoc} 160 */ 161 @Override 162 public void testLog(String dataName, LogDataType dataType, InputStreamSource dataStream) { 163 // save as zip file in report dir 164 // TODO: ensure uniqueness of file name 165 // TODO: use dataType.getFileExt() when its made public 166 String fileName = String.format("%s.%s", dataName, dataType.name().toLowerCase()); 167 // TODO: consider compressing large files 168 File logFile = new File(mReportDir, fileName); 169 try { 170 FileUtil.writeToFile(dataStream.createInputStream(), logFile); 171 } catch (IOException e) { 172 Log.e(LOG_TAG, String.format("Failed to write log %s", logFile.getAbsolutePath())); 173 } 174 } 175 176 /** 177 * {@inheritDoc} 178 */ 179 @Override 180 public void testRunStarted(String name, int numTests) { 181 if (mCurrentPkgResult != null && !name.equals(mCurrentPkgResult.getAppPackageName())) { 182 // display results from previous run 183 logCompleteRun(mCurrentPkgResult); 184 } 185 if (name.equals(DeviceInfoCollector.APP_PACKAGE_NAME)) { 186 logResult("Collecting device info"); 187 } else if (mCurrentPkgResult == null || !name.equals( 188 mCurrentPkgResult.getAppPackageName())) { 189 logResult("-----------------------------------------"); 190 logResult("Test package %s started", name); 191 logResult("-----------------------------------------"); 192 } 193 mCurrentPkgResult = mResults.getOrCreatePackage(name); 194 } 195 196 /** 197 * {@inheritDoc} 198 */ 199 @Override 200 public void testStarted(TestIdentifier test) { 201 mCurrentPkgResult.insertTest(test); 202 } 203 204 /** 205 * {@inheritDoc} 206 */ 207 @Override 208 public void testFailed(TestFailure status, TestIdentifier test, String trace) { 209 mCurrentPkgResult.reportTestFailure(test, CtsTestStatus.FAIL, trace); 210 } 211 212 /** 213 * {@inheritDoc} 214 */ 215 @Override 216 public void testEnded(TestIdentifier test, Map<String, String> testMetrics) { 217 mCurrentPkgResult.reportTestEnded(test); 218 Test result = mCurrentPkgResult.findTest(test); 219 String stack = result.getStackTrace() == null ? "" : "\n" + result.getStackTrace(); 220 logResult("%s#%s %s %s", test.getClassName(), test.getTestName(), result.getResult(), 221 stack); 222 } 223 224 /** 225 * {@inheritDoc} 226 */ 227 @Override 228 public void testRunEnded(long elapsedTime, Map<String, String> runMetrics) { 229 mCurrentPkgResult.populateMetrics(runMetrics); 230 } 231 232 /** 233 * {@inheritDoc} 234 */ 235 @Override 236 public void invocationEnded(long elapsedTime) { 237 // display the results of the last completed run 238 if (mCurrentPkgResult != null) { 239 logCompleteRun(mCurrentPkgResult); 240 } 241 createXmlResult(mReportDir, mStartTime, elapsedTime); 242 copyFormattingFiles(mReportDir); 243 zipResults(mReportDir); 244 } 245 246 private void logResult(String format, Object... args) { 247 if (mQuietOutput) { 248 CLog.i(format, args); 249 } else { 250 Log.logAndDisplay(LogLevel.INFO, mDeviceSerial, String.format(format, args)); 251 } 252 } 253 254 private void logCompleteRun(TestPackageResult pkgResult) { 255 if (pkgResult.getAppPackageName().equals(DeviceInfoCollector.APP_PACKAGE_NAME)) { 256 logResult("Device info collection complete"); 257 return; 258 } 259 logResult("%s package complete: Passed %d, Failed %d, Not Executed %d", 260 pkgResult.getAppPackageName(), pkgResult.countTests(CtsTestStatus.PASS), 261 pkgResult.countTests(CtsTestStatus.FAIL), 262 pkgResult.countTests(CtsTestStatus.NOT_EXECUTED)); 263 } 264 265 /** 266 * Creates a report file and populates it with the report data from the completed tests. 267 */ 268 private void createXmlResult(File reportDir, String startTimestamp, long elapsedTime) { 269 String endTime = getTimestamp(); 270 271 OutputStream stream = null; 272 try { 273 stream = createOutputResultStream(reportDir); 274 KXmlSerializer serializer = new KXmlSerializer(); 275 serializer.setOutput(stream, "UTF-8"); 276 serializer.startDocument("UTF-8", false); 277 serializer.setFeature( 278 "http://xmlpull.org/v1/doc/features.html#indent-output", true); 279 serializer.processingInstruction("xml-stylesheet type=\"text/xsl\" " + 280 "href=\"cts_result.xsl\""); 281 serializeResultsDoc(serializer, startTimestamp, endTime); 282 serializer.endDocument(); 283 String msg = String.format("XML test result file generated at %s. Passed %d, " + 284 "Failed %d, Not Executed %d", mReportDir.getName(), 285 mResults.countTests(CtsTestStatus.PASS), 286 mResults.countTests(CtsTestStatus.FAIL), 287 mResults.countTests(CtsTestStatus.NOT_EXECUTED)); 288 logResult(msg); 289 logResult("Time: %s", TimeUtil.formatElapsedTime(elapsedTime)); 290 } catch (IOException e) { 291 Log.e(LOG_TAG, "Failed to generate report data"); 292 } finally { 293 StreamUtil.closeStream(stream); 294 } 295 } 296 297 /** 298 * Output the results XML. 299 * 300 * @param serializer the {@link KXmlSerializer} to use 301 * @param startTime the user-friendly starting time of the test invocation 302 * @param endTime the user-friendly ending time of the test invocation 303 * @throws IOException 304 */ 305 private void serializeResultsDoc(KXmlSerializer serializer, String startTime, String endTime) 306 throws IOException { 307 serializer.startTag(ns, RESULT_TAG); 308 serializer.attribute(ns, PLAN_ATTR, mPlanName); 309 serializer.attribute(ns, "starttime", startTime); 310 serializer.attribute(ns, "endtime", endTime); 311 serializer.attribute(ns, "version", CTS_RESULT_FILE_VERSION); 312 313 serializeDeviceInfo(serializer); 314 serializeHostInfo(serializer); 315 serializeTestSummary(serializer); 316 mResults.serialize(serializer); 317 // TODO: not sure why, but the serializer doesn't like this statement 318 //serializer.endTag(ns, RESULT_TAG); 319 } 320 321 /** 322 * Output the device info XML. 323 * 324 * @param serializer 325 */ 326 private void serializeDeviceInfo(KXmlSerializer serializer) throws IOException { 327 serializer.startTag(ns, "DeviceInfo"); 328 329 Map<String, String> deviceInfoMetrics = mResults.getDeviceInfoMetrics(); 330 if (deviceInfoMetrics == null || deviceInfoMetrics.isEmpty()) { 331 // this might be expected, if device info collection was turned off 332 CLog.d("Could not find device info"); 333 return; 334 } 335 336 // Extract metrics that need extra handling, and then dump the remainder into BuildInfo 337 Map<String, String> metricsCopy = new HashMap<String, String>( 338 deviceInfoMetrics); 339 serializer.startTag(ns, "Screen"); 340 String screenWidth = metricsCopy.remove(DeviceInfoConstants.SCREEN_WIDTH); 341 String screenHeight = metricsCopy.remove(DeviceInfoConstants.SCREEN_HEIGHT); 342 serializer.attribute(ns, "resolution", String.format("%sx%s", screenWidth, screenHeight)); 343 serializer.attribute(ns, DeviceInfoConstants.SCREEN_DENSITY, 344 metricsCopy.remove(DeviceInfoConstants.SCREEN_DENSITY)); 345 serializer.attribute(ns, DeviceInfoConstants.SCREEN_DENSITY_BUCKET, 346 metricsCopy.remove(DeviceInfoConstants.SCREEN_DENSITY_BUCKET)); 347 serializer.attribute(ns, DeviceInfoConstants.SCREEN_SIZE, 348 metricsCopy.remove(DeviceInfoConstants.SCREEN_SIZE)); 349 serializer.endTag(ns, "Screen"); 350 351 serializer.startTag(ns, "PhoneSubInfo"); 352 serializer.attribute(ns, "subscriberId", metricsCopy.remove( 353 DeviceInfoConstants.PHONE_NUMBER)); 354 serializer.endTag(ns, "PhoneSubInfo"); 355 356 String featureData = metricsCopy.remove(DeviceInfoConstants.FEATURES); 357 String processData = metricsCopy.remove(DeviceInfoConstants.PROCESSES); 358 359 // dump the remaining metrics without translation 360 serializer.startTag(ns, "BuildInfo"); 361 for (Map.Entry<String, String> metricEntry : metricsCopy.entrySet()) { 362 serializer.attribute(ns, metricEntry.getKey(), metricEntry.getValue()); 363 } 364 serializer.attribute(ns, "deviceID", mDeviceSerial); 365 serializer.endTag(ns, "BuildInfo"); 366 367 serializeFeatureInfo(serializer, featureData); 368 serializeProcessInfo(serializer, processData); 369 370 serializer.endTag(ns, "DeviceInfo"); 371 } 372 373 /** 374 * Prints XML indicating what features are supported by the device. It parses a string from the 375 * featureData argument that is in the form of "feature1:true;feature2:false;featuer3;true;" 376 * with a trailing semi-colon. 377 * 378 * <pre> 379 * <FeatureInfo> 380 * <Feature name="android.name.of.feature" available="true" /> 381 * ... 382 * </FeatureInfo> 383 * </pre> 384 * 385 * @param serializer used to create XML 386 * @param featureData raw unparsed feature data 387 */ 388 private void serializeFeatureInfo(KXmlSerializer serializer, String featureData) throws IOException { 389 serializer.startTag(ns, "FeatureInfo"); 390 391 if (featureData == null) { 392 featureData = ""; 393 } 394 395 String[] featurePairs = featureData.split(";"); 396 for (String featurePair : featurePairs) { 397 String[] nameTypeAvailability = featurePair.split(":"); 398 if (nameTypeAvailability.length >= 3) { 399 serializer.startTag(ns, "Feature"); 400 serializer.attribute(ns, "name", nameTypeAvailability[0]); 401 serializer.attribute(ns, "type", nameTypeAvailability[1]); 402 serializer.attribute(ns, "available", nameTypeAvailability[2]); 403 serializer.endTag(ns, "Feature"); 404 } 405 } 406 serializer.endTag(ns, "FeatureInfo"); 407 } 408 409 /** 410 * Prints XML data indicating what particular processes of interest were running on the device. 411 * It parses a string from the rootProcesses argument that is in the form of 412 * "processName1;processName2;..." with a trailing semi-colon. 413 * 414 * <pre> 415 * <ProcessInfo> 416 * <Process name="long_cat_viewer" uid="0" /> 417 * ... 418 * </ProcessInfo> 419 * </pre> 420 */ 421 private void serializeProcessInfo(KXmlSerializer serializer, String rootProcesses) 422 throws IOException { 423 serializer.startTag(ns, "ProcessInfo"); 424 425 if (rootProcesses == null) { 426 rootProcesses = ""; 427 } 428 429 String[] processNames = rootProcesses.split(";"); 430 for (String processName : processNames) { 431 processName = processName.trim(); 432 if (processName.length() > 0) { 433 serializer.startTag(ns, "Process"); 434 serializer.attribute(ns, "name", processName); 435 serializer.attribute(ns, "uid", "0"); 436 serializer.endTag(ns, "Process"); 437 } 438 } 439 serializer.endTag(ns, "ProcessInfo"); 440 } 441 442 /** 443 * Output the host info XML. 444 * 445 * @param serializer 446 */ 447 private void serializeHostInfo(KXmlSerializer serializer) throws IOException { 448 serializer.startTag(ns, "HostInfo"); 449 450 String hostName = ""; 451 try { 452 hostName = InetAddress.getLocalHost().getHostName(); 453 } catch (UnknownHostException ignored) {} 454 serializer.attribute(ns, "name", hostName); 455 456 serializer.startTag(ns, "Os"); 457 serializer.attribute(ns, "name", System.getProperty("os.name")); 458 serializer.attribute(ns, "version", System.getProperty("os.version")); 459 serializer.attribute(ns, "arch", System.getProperty("os.arch")); 460 serializer.endTag(ns, "Os"); 461 462 serializer.startTag(ns, "Java"); 463 serializer.attribute(ns, "name", System.getProperty("java.vendor")); 464 serializer.attribute(ns, "version", System.getProperty("java.version")); 465 serializer.endTag(ns, "Java"); 466 467 serializer.startTag(ns, "Cts"); 468 serializer.attribute(ns, "version", CtsBuildProvider.CTS_BUILD_VERSION); 469 // TODO: consider outputting other tradefed options here 470 serializer.startTag(ns, "IntValue"); 471 serializer.attribute(ns, "name", "testStatusTimeoutMs"); 472 // TODO: create a constant variable for testStatusTimeoutMs value. Currently it cannot be 473 // changed 474 serializer.attribute(ns, "value", "600000"); 475 serializer.endTag(ns, "IntValue"); 476 serializer.endTag(ns, "Cts"); 477 478 serializer.endTag(ns, "HostInfo"); 479 } 480 481 /** 482 * Output the test summary XML containing summary totals for all tests. 483 * 484 * @param serializer 485 * @throws IOException 486 */ 487 private void serializeTestSummary(KXmlSerializer serializer) throws IOException { 488 serializer.startTag(ns, SUMMARY_TAG); 489 serializer.attribute(ns, FAILED_ATTR, Integer.toString(mResults.countTests( 490 CtsTestStatus.FAIL))); 491 serializer.attribute(ns, NOT_EXECUTED_ATTR, Integer.toString(mResults.countTests( 492 CtsTestStatus.NOT_EXECUTED))); 493 // ignore timeouts - these are reported as errors 494 serializer.attribute(ns, TIMEOUT_ATTR, "0"); 495 serializer.attribute(ns, PASS_ATTR, Integer.toString(mResults.countTests( 496 CtsTestStatus.PASS))); 497 serializer.endTag(ns, SUMMARY_TAG); 498 } 499 500 /** 501 * Creates the output stream to use for test results. Exposed for mocking. 502 */ 503 OutputStream createOutputResultStream(File reportDir) throws IOException { 504 File reportFile = new File(reportDir, TEST_RESULT_FILE_NAME); 505 logResult("Created xml report file at file://%s", reportFile.getAbsolutePath()); 506 return new FileOutputStream(reportFile); 507 } 508 509 /** 510 * Copy the xml formatting files stored in this jar to the results directory 511 * 512 * @param resultsDir 513 */ 514 private void copyFormattingFiles(File resultsDir) { 515 for (String resultFileName : CTS_RESULT_RESOURCES) { 516 InputStream configStream = getClass().getResourceAsStream(String.format("/%s", 517 resultFileName)); 518 if (configStream != null) { 519 File resultFile = new File(resultsDir, resultFileName); 520 try { 521 FileUtil.writeToFile(configStream, resultFile); 522 } catch (IOException e) { 523 Log.w(LOG_TAG, String.format("Failed to write %s to file", resultFileName)); 524 } 525 } else { 526 Log.w(LOG_TAG, String.format("Failed to load %s from jar", resultFileName)); 527 } 528 } 529 } 530 531 /** 532 * Zip the contents of the given results directory. 533 * 534 * @param resultsDir 535 */ 536 private void zipResults(File resultsDir) { 537 try { 538 // create a file in parent directory, with same name as resultsDir 539 File zipResultFile = new File(resultsDir.getParent(), String.format("%s.zip", 540 resultsDir.getName())); 541 FileUtil.createZip(resultsDir, zipResultFile); 542 } catch (IOException e) { 543 Log.w(LOG_TAG, String.format("Failed to create zip for %s", resultsDir.getName())); 544 } 545 } 546 547 /** 548 * Get a String version of the current time. 549 * <p/> 550 * Exposed so unit tests can mock. 551 */ 552 String getTimestamp() { 553 return TimeUtil.getTimestamp(); 554 } 555 556 /** 557 * {@inheritDoc} 558 */ 559 @Override 560 public void testRunFailed(String errorMessage) { 561 // ignore 562 } 563 564 /** 565 * {@inheritDoc} 566 */ 567 @Override 568 public void testRunStopped(long elapsedTime) { 569 // ignore 570 } 571 572 /** 573 * {@inheritDoc} 574 */ 575 @Override 576 public void invocationFailed(Throwable cause) { 577 // ignore 578 } 579 580 /** 581 * {@inheritDoc} 582 */ 583 @Override 584 public TestSummary getSummary() { 585 return null; 586 } 587} 588