1 /* 2 * Copyright (C) 2008 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.ddmlib.testrunner; 18 19 import com.android.ddmlib.IShellOutputReceiver; 20 import com.android.ddmlib.Log; 21 import com.android.ddmlib.MultiLineReceiver; 22 23 import java.util.ArrayList; 24 import java.util.Collection; 25 import java.util.Collections; 26 import java.util.HashMap; 27 import java.util.HashSet; 28 import java.util.Map; 29 import java.util.Set; 30 import java.util.regex.Matcher; 31 import java.util.regex.Pattern; 32 33 /** 34 * Parses the 'raw output mode' results of an instrumentation test run from shell and informs a 35 * ITestRunListener of the results. 36 * 37 * <p>Expects the following output: 38 * 39 * <p>If fatal error occurred when attempted to run the tests: 40 * <pre> 41 * INSTRUMENTATION_STATUS: Error=error Message 42 * INSTRUMENTATION_FAILED: 43 * </pre> 44 * <p>or 45 * <pre> 46 * INSTRUMENTATION_RESULT: shortMsg=error Message 47 * </pre> 48 * 49 * <p>Otherwise, expect a series of test results, each one containing a set of status key/value 50 * pairs, delimited by a start(1)/pass(0)/fail(-2)/error(-1) status code result. At end of test 51 * run, expects that the elapsed test time in seconds will be displayed 52 * 53 * <p>For example: 54 * <pre> 55 * INSTRUMENTATION_STATUS_CODE: 1 56 * INSTRUMENTATION_STATUS: class=com.foo.FooTest 57 * INSTRUMENTATION_STATUS: test=testFoo 58 * INSTRUMENTATION_STATUS: numtests=2 59 * INSTRUMENTATION_STATUS: stack=com.foo.FooTest#testFoo:312 60 * com.foo.X 61 * INSTRUMENTATION_STATUS_CODE: -2 62 * ... 63 * 64 * Time: X 65 * </pre> 66 * <p>Note that the "value" portion of the key-value pair may wrap over several text lines 67 */ 68 public class InstrumentationResultParser extends MultiLineReceiver { 69 70 /** Relevant test status keys. */ 71 private static class StatusKeys { 72 private static final String TEST = "test"; 73 private static final String CLASS = "class"; 74 private static final String STACK = "stack"; 75 private static final String NUMTESTS = "numtests"; 76 private static final String ERROR = "Error"; 77 private static final String SHORTMSG = "shortMsg"; 78 } 79 80 /** The set of expected status keys. Used to filter which keys should be stored as metrics */ 81 private static final Set<String> KNOWN_KEYS = new HashSet<String>(); 82 static { 83 KNOWN_KEYS.add(StatusKeys.TEST); 84 KNOWN_KEYS.add(StatusKeys.CLASS); 85 KNOWN_KEYS.add(StatusKeys.STACK); 86 KNOWN_KEYS.add(StatusKeys.NUMTESTS); 87 KNOWN_KEYS.add(StatusKeys.ERROR); 88 KNOWN_KEYS.add(StatusKeys.SHORTMSG); 89 // unused, but regularly occurring status keys. 90 KNOWN_KEYS.add("stream"); 91 KNOWN_KEYS.add("id"); 92 KNOWN_KEYS.add("current"); 93 } 94 95 /** Test result status codes. */ 96 private static class StatusCodes { 97 private static final int FAILURE = -2; 98 private static final int START = 1; 99 private static final int ERROR = -1; 100 private static final int OK = 0; 101 private static final int IN_PROGRESS = 2; 102 } 103 104 /** Prefixes used to identify output. */ 105 private static class Prefixes { 106 private static final String STATUS = "INSTRUMENTATION_STATUS: "; 107 private static final String STATUS_CODE = "INSTRUMENTATION_STATUS_CODE: "; 108 private static final String STATUS_FAILED = "INSTRUMENTATION_FAILED: "; 109 private static final String CODE = "INSTRUMENTATION_CODE: "; 110 private static final String RESULT = "INSTRUMENTATION_RESULT: "; 111 private static final String TIME_REPORT = "Time: "; 112 } 113 114 private final Collection<ITestRunListener> mTestListeners; 115 116 /** 117 * Test result data 118 */ 119 private static class TestResult { 120 private Integer mCode = null; 121 private String mTestName = null; 122 private String mTestClass = null; 123 private String mStackTrace = null; 124 private Integer mNumTests = null; 125 126 /** Returns true if all expected values have been parsed */ isComplete()127 boolean isComplete() { 128 return mCode != null && mTestName != null && mTestClass != null; 129 } 130 131 /** Provides a more user readable string for TestResult, if possible */ 132 @Override toString()133 public String toString() { 134 StringBuilder output = new StringBuilder(); 135 if (mTestClass != null ) { 136 output.append(mTestClass); 137 output.append('#'); 138 } 139 if (mTestName != null) { 140 output.append(mTestName); 141 } 142 if (output.length() > 0) { 143 return output.toString(); 144 } 145 return "unknown result"; 146 } 147 } 148 149 /** the name to provide to {@link ITestRunListener#testRunStarted(String, int)} */ 150 private final String mTestRunName; 151 152 /** Stores the status values for the test result currently being parsed */ 153 private TestResult mCurrentTestResult = null; 154 155 /** Stores the status values for the test result last parsed */ 156 private TestResult mLastTestResult = null; 157 158 /** Stores the current "key" portion of the status key-value being parsed. */ 159 private String mCurrentKey = null; 160 161 /** Stores the current "value" portion of the status key-value being parsed. */ 162 private StringBuilder mCurrentValue = null; 163 164 /** True if start of test has already been reported to listener. */ 165 private boolean mTestStartReported = false; 166 167 /** True if the completion of the test run has been detected. */ 168 private boolean mTestRunFinished = false; 169 170 /** True if test run failure has already been reported to listener. */ 171 private boolean mTestRunFailReported = false; 172 173 /** The elapsed time of the test run, in milliseconds. */ 174 private long mTestTime = 0; 175 176 /** True if current test run has been canceled by user. */ 177 private boolean mIsCancelled = false; 178 179 /** The number of tests currently run */ 180 private int mNumTestsRun = 0; 181 182 /** The number of tests expected to run */ 183 private int mNumTestsExpected = 0; 184 185 /** True if the parser is parsing a line beginning with "INSTRUMENTATION_RESULT" */ 186 private boolean mInInstrumentationResultKey = false; 187 188 /** 189 * Stores key-value pairs under INSTRUMENTATION_RESULT header, these are printed at the 190 * end of a test run, if applicable 191 */ 192 private Map<String, String> mInstrumentationResultBundle = new HashMap<String, String>(); 193 194 /** 195 * Stores key-value pairs of metrics emitted during the execution of each test case. Note that 196 * standard keys that are stored in the TestResults class are filtered out of this Map. 197 */ 198 private Map<String, String> mTestMetrics = new HashMap<String, String>(); 199 200 private static final String LOG_TAG = "InstrumentationResultParser"; 201 202 /** Error message supplied when no parseable test results are received from test run. */ 203 static final String NO_TEST_RESULTS_MSG = "No test results"; 204 205 /** Error message supplied when a test start bundle is parsed, but not the test end bundle. */ 206 static final String INCOMPLETE_TEST_ERR_MSG_PREFIX = "Test failed to run to completion"; 207 static final String INCOMPLETE_TEST_ERR_MSG_POSTFIX = "Check device logcat for details"; 208 209 /** Error message supplied when the test run is incomplete. */ 210 static final String INCOMPLETE_RUN_ERR_MSG_PREFIX = "Test run failed to complete"; 211 212 /** 213 * Creates the InstrumentationResultParser. 214 * 215 * @param runName the test run name to provide to 216 * {@link ITestRunListener#testRunStarted(String, int)} 217 * @param listeners informed of test results as the tests are executing 218 */ InstrumentationResultParser(String runName, Collection<ITestRunListener> listeners)219 public InstrumentationResultParser(String runName, Collection<ITestRunListener> listeners) { 220 mTestRunName = runName; 221 mTestListeners = new ArrayList<ITestRunListener>(listeners); 222 } 223 224 /** 225 * Creates the InstrumentationResultParser for a single listener. 226 * 227 * @param runName the test run name to provide to 228 * {@link ITestRunListener#testRunStarted(String, int)} 229 * @param listener informed of test results as the tests are executing 230 */ InstrumentationResultParser(String runName, ITestRunListener listener)231 public InstrumentationResultParser(String runName, ITestRunListener listener) { 232 this(runName, Collections.singletonList(listener)); 233 } 234 235 /** 236 * Processes the instrumentation test output from shell. 237 * 238 * @see MultiLineReceiver#processNewLines 239 */ 240 @Override processNewLines(String[] lines)241 public void processNewLines(String[] lines) { 242 for (String line : lines) { 243 parse(line); 244 // in verbose mode, dump all adb output to log 245 Log.v(LOG_TAG, line); 246 } 247 } 248 249 /** 250 * Parse an individual output line. Expects a line that is one of: 251 * <ul> 252 * <li> 253 * The start of a new status line (starts with Prefixes.STATUS or Prefixes.STATUS_CODE), 254 * and thus there is a new key=value pair to parse, and the previous key-value pair is 255 * finished. 256 * </li> 257 * <li> 258 * A continuation of the previous status (the "value" portion of the key has wrapped 259 * to the next line). 260 * </li> 261 * <li> A line reporting a fatal error in the test run (Prefixes.STATUS_FAILED) </li> 262 * <li> A line reporting the total elapsed time of the test run. (Prefixes.TIME_REPORT) </li> 263 * </ul> 264 * 265 * @param line Text output line 266 */ parse(String line)267 private void parse(String line) { 268 if (line.startsWith(Prefixes.STATUS_CODE)) { 269 // Previous status key-value has been collected. Store it. 270 submitCurrentKeyValue(); 271 mInInstrumentationResultKey = false; 272 parseStatusCode(line); 273 } else if (line.startsWith(Prefixes.STATUS)) { 274 // Previous status key-value has been collected. Store it. 275 submitCurrentKeyValue(); 276 mInInstrumentationResultKey = false; 277 parseKey(line, Prefixes.STATUS.length()); 278 } else if (line.startsWith(Prefixes.RESULT)) { 279 // Previous status key-value has been collected. Store it. 280 submitCurrentKeyValue(); 281 mInInstrumentationResultKey = true; 282 parseKey(line, Prefixes.RESULT.length()); 283 } else if (line.startsWith(Prefixes.STATUS_FAILED) || 284 line.startsWith(Prefixes.CODE)) { 285 // Previous status key-value has been collected. Store it. 286 submitCurrentKeyValue(); 287 mInInstrumentationResultKey = false; 288 // these codes signal the end of the instrumentation run 289 mTestRunFinished = true; 290 // just ignore the remaining data on this line 291 } else if (line.startsWith(Prefixes.TIME_REPORT)) { 292 parseTime(line); 293 } else { 294 if (mCurrentValue != null) { 295 // this is a value that has wrapped to next line. 296 mCurrentValue.append("\r\n"); 297 mCurrentValue.append(line); 298 } else if (line.trim().length() > 0){ 299 Log.d(LOG_TAG, "unrecognized line " + line); 300 } 301 } 302 } 303 304 /** 305 * Stores the currently parsed key-value pair in the appropriate place. 306 */ submitCurrentKeyValue()307 private void submitCurrentKeyValue() { 308 if (mCurrentKey != null && mCurrentValue != null) { 309 String statusValue = mCurrentValue.toString(); 310 if (mInInstrumentationResultKey) { 311 if (!KNOWN_KEYS.contains(mCurrentKey)) { 312 mInstrumentationResultBundle.put(mCurrentKey, statusValue); 313 } else if (mCurrentKey.equals(StatusKeys.SHORTMSG)) { 314 // test run must have failed 315 handleTestRunFailed(String.format("Instrumentation run failed due to '%1$s'", 316 statusValue)); 317 } 318 } else { 319 TestResult testInfo = getCurrentTestInfo(); 320 321 if (mCurrentKey.equals(StatusKeys.CLASS)) { 322 testInfo.mTestClass = statusValue.trim(); 323 } else if (mCurrentKey.equals(StatusKeys.TEST)) { 324 testInfo.mTestName = statusValue.trim(); 325 } else if (mCurrentKey.equals(StatusKeys.NUMTESTS)) { 326 try { 327 testInfo.mNumTests = Integer.parseInt(statusValue); 328 } catch (NumberFormatException e) { 329 Log.w(LOG_TAG, "Unexpected integer number of tests, received " 330 + statusValue); 331 } 332 } else if (mCurrentKey.equals(StatusKeys.ERROR)) { 333 // test run must have failed 334 handleTestRunFailed(statusValue); 335 } else if (mCurrentKey.equals(StatusKeys.STACK)) { 336 testInfo.mStackTrace = statusValue; 337 } else if (!KNOWN_KEYS.contains(mCurrentKey)) { 338 // Not one of the recognized key/value pairs, so dump it in mTestMetrics 339 mTestMetrics.put(mCurrentKey, statusValue); 340 } 341 } 342 343 mCurrentKey = null; 344 mCurrentValue = null; 345 } 346 } 347 348 /** 349 * A utility method to return the test metrics from the current test case execution and get 350 * ready for the next one. 351 */ getAndResetTestMetrics()352 private Map<String, String> getAndResetTestMetrics() { 353 Map<String, String> retVal = mTestMetrics; 354 mTestMetrics = new HashMap<String, String>(); 355 return retVal; 356 } 357 getCurrentTestInfo()358 private TestResult getCurrentTestInfo() { 359 if (mCurrentTestResult == null) { 360 mCurrentTestResult = new TestResult(); 361 } 362 return mCurrentTestResult; 363 } 364 clearCurrentTestInfo()365 private void clearCurrentTestInfo() { 366 mLastTestResult = mCurrentTestResult; 367 mCurrentTestResult = null; 368 } 369 370 /** 371 * Parses the key from the current line. 372 * Expects format of "key=value". 373 * 374 * @param line full line of text to parse 375 * @param keyStartPos the starting position of the key in the given line 376 */ parseKey(String line, int keyStartPos)377 private void parseKey(String line, int keyStartPos) { 378 int endKeyPos = line.indexOf('=', keyStartPos); 379 if (endKeyPos != -1) { 380 mCurrentKey = line.substring(keyStartPos, endKeyPos).trim(); 381 parseValue(line, endKeyPos + 1); 382 } 383 } 384 385 /** 386 * Parses the start of a key=value pair. 387 * 388 * @param line - full line of text to parse 389 * @param valueStartPos - the starting position of the value in the given line 390 */ parseValue(String line, int valueStartPos)391 private void parseValue(String line, int valueStartPos) { 392 mCurrentValue = new StringBuilder(); 393 mCurrentValue.append(line.substring(valueStartPos)); 394 } 395 396 /** 397 * Parses out a status code result. 398 */ parseStatusCode(String line)399 private void parseStatusCode(String line) { 400 String value = line.substring(Prefixes.STATUS_CODE.length()).trim(); 401 TestResult testInfo = getCurrentTestInfo(); 402 testInfo.mCode = StatusCodes.ERROR; 403 try { 404 testInfo.mCode = Integer.parseInt(value); 405 } catch (NumberFormatException e) { 406 Log.w(LOG_TAG, "Expected integer status code, received: " + value); 407 testInfo.mCode = StatusCodes.ERROR; 408 } 409 if (testInfo.mCode != StatusCodes.IN_PROGRESS) { 410 // this means we're done with current test result bundle 411 reportResult(testInfo); 412 clearCurrentTestInfo(); 413 } 414 } 415 416 /** 417 * Returns true if test run canceled. 418 * 419 * @see IShellOutputReceiver#isCancelled() 420 */ 421 @Override isCancelled()422 public boolean isCancelled() { 423 return mIsCancelled; 424 } 425 426 /** 427 * Requests cancellation of test run. 428 */ cancel()429 public void cancel() { 430 mIsCancelled = true; 431 } 432 433 /** 434 * Reports a test result to the test run listener. Must be called when a individual test 435 * result has been fully parsed. 436 * 437 * @param statusMap key-value status pairs of test result 438 */ reportResult(TestResult testInfo)439 private void reportResult(TestResult testInfo) { 440 if (!testInfo.isComplete()) { 441 Log.w(LOG_TAG, "invalid instrumentation status bundle " + testInfo.toString()); 442 return; 443 } 444 reportTestRunStarted(testInfo); 445 TestIdentifier testId = new TestIdentifier(testInfo.mTestClass, testInfo.mTestName); 446 Map<String, String> metrics; 447 448 switch (testInfo.mCode) { 449 case StatusCodes.START: 450 for (ITestRunListener listener : mTestListeners) { 451 listener.testStarted(testId); 452 } 453 break; 454 case StatusCodes.FAILURE: 455 metrics = getAndResetTestMetrics(); 456 for (ITestRunListener listener : mTestListeners) { 457 listener.testFailed(ITestRunListener.TestFailure.FAILURE, testId, 458 getTrace(testInfo)); 459 460 listener.testEnded(testId, metrics); 461 } 462 mNumTestsRun++; 463 break; 464 case StatusCodes.ERROR: 465 metrics = getAndResetTestMetrics(); 466 for (ITestRunListener listener : mTestListeners) { 467 listener.testFailed(ITestRunListener.TestFailure.ERROR, testId, 468 getTrace(testInfo)); 469 listener.testEnded(testId, metrics); 470 } 471 mNumTestsRun++; 472 break; 473 case StatusCodes.OK: 474 metrics = getAndResetTestMetrics(); 475 for (ITestRunListener listener : mTestListeners) { 476 listener.testEnded(testId, metrics); 477 } 478 mNumTestsRun++; 479 break; 480 default: 481 metrics = getAndResetTestMetrics(); 482 Log.e(LOG_TAG, "Unknown status code received: " + testInfo.mCode); 483 for (ITestRunListener listener : mTestListeners) { 484 listener.testEnded(testId, metrics); 485 } 486 mNumTestsRun++; 487 break; 488 } 489 490 } 491 492 /** 493 * Reports the start of a test run, and the total test count, if it has not been previously 494 * reported. 495 * 496 * @param testInfo current test status values 497 */ reportTestRunStarted(TestResult testInfo)498 private void reportTestRunStarted(TestResult testInfo) { 499 // if start test run not reported yet 500 if (!mTestStartReported && testInfo.mNumTests != null) { 501 for (ITestRunListener listener : mTestListeners) { 502 listener.testRunStarted(mTestRunName, testInfo.mNumTests); 503 } 504 mNumTestsExpected = testInfo.mNumTests; 505 mTestStartReported = true; 506 } 507 } 508 509 /** 510 * Returns the stack trace of the current failed test, from the provided testInfo. 511 */ getTrace(TestResult testInfo)512 private String getTrace(TestResult testInfo) { 513 if (testInfo.mStackTrace != null) { 514 return testInfo.mStackTrace; 515 } else { 516 Log.e(LOG_TAG, "Could not find stack trace for failed test "); 517 return new Throwable("Unknown failure").toString(); 518 } 519 } 520 521 /** 522 * Parses out and store the elapsed time. 523 */ parseTime(String line)524 private void parseTime(String line) { 525 final Pattern timePattern = Pattern.compile(String.format("%s\\s*([\\d\\.]+)", 526 Prefixes.TIME_REPORT)); 527 Matcher timeMatcher = timePattern.matcher(line); 528 if (timeMatcher.find()) { 529 String timeString = timeMatcher.group(1); 530 try { 531 float timeSeconds = Float.parseFloat(timeString); 532 mTestTime = (long) (timeSeconds * 1000); 533 } catch (NumberFormatException e) { 534 Log.w(LOG_TAG, String.format("Unexpected time format %1$s", line)); 535 } 536 } else { 537 Log.w(LOG_TAG, String.format("Unexpected time format %1$s", line)); 538 } 539 } 540 541 /** 542 * Inform the parser of a instrumentation run failure. Should be called when the adb command 543 * used to run the test fails. 544 */ handleTestRunFailed(String errorMsg)545 public void handleTestRunFailed(String errorMsg) { 546 errorMsg = (errorMsg == null ? "Unknown error" : errorMsg); 547 Log.i(LOG_TAG, String.format("test run failed: '%1$s'", errorMsg)); 548 if (mLastTestResult != null && 549 mLastTestResult.isComplete() && 550 StatusCodes.START == mLastTestResult.mCode) { 551 552 // received test start msg, but not test complete 553 // assume test caused this, report as test failure 554 TestIdentifier testId = new TestIdentifier(mLastTestResult.mTestClass, 555 mLastTestResult.mTestName); 556 for (ITestRunListener listener : mTestListeners) { 557 listener.testFailed(ITestRunListener.TestFailure.ERROR, testId, 558 String.format("%1$s. Reason: '%2$s'. %3$s", INCOMPLETE_TEST_ERR_MSG_PREFIX, 559 errorMsg, INCOMPLETE_TEST_ERR_MSG_POSTFIX)); 560 listener.testEnded(testId, getAndResetTestMetrics()); 561 } 562 } 563 for (ITestRunListener listener : mTestListeners) { 564 if (!mTestStartReported) { 565 // test run wasn't started - must have crashed before it started 566 listener.testRunStarted(mTestRunName, 0); 567 } 568 listener.testRunFailed(errorMsg); 569 listener.testRunEnded(mTestTime, mInstrumentationResultBundle); 570 } 571 mTestStartReported = true; 572 mTestRunFailReported = true; 573 } 574 575 /** 576 * Called by parent when adb session is complete. 577 */ 578 @Override done()579 public void done() { 580 super.done(); 581 if (!mTestRunFailReported) { 582 handleOutputDone(); 583 } 584 } 585 586 /** 587 * Handles the end of the adb session when a test run failure has not been reported yet 588 */ handleOutputDone()589 private void handleOutputDone() { 590 if (!mTestStartReported && !mTestRunFinished) { 591 // no results 592 handleTestRunFailed(NO_TEST_RESULTS_MSG); 593 } else if (mNumTestsExpected > mNumTestsRun) { 594 final String message = 595 String.format("%1$s. Expected %2$d tests, received %3$d", 596 INCOMPLETE_RUN_ERR_MSG_PREFIX, mNumTestsExpected, mNumTestsRun); 597 handleTestRunFailed(message); 598 } else { 599 for (ITestRunListener listener : mTestListeners) { 600 if (!mTestStartReported) { 601 // test run wasn't started, but it finished successfully. Must be a run with 602 // no tests 603 listener.testRunStarted(mTestRunName, 0); 604 } 605 listener.testRunEnded(mTestTime, mInstrumentationResultBundle); 606 } 607 } 608 } 609 } 610