1 /* 2 * Copyright (C) 2011 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 package com.android.cts.tradefed.testtype; 17 18 import com.android.ddmlib.IShellOutputReceiver; 19 import com.android.ddmlib.Log; 20 import com.android.ddmlib.MultiLineReceiver; 21 import com.android.ddmlib.testrunner.ITestRunListener; 22 import com.android.ddmlib.testrunner.TestIdentifier; 23 import com.android.tradefed.testtype.testdefs.XmlDefsTest; 24 25 import java.util.ArrayList; 26 import java.util.Collection; 27 import java.util.Collections; 28 import java.util.HashMap; 29 import java.util.Map; 30 import java.util.regex.Matcher; 31 import java.util.regex.Pattern; 32 33 /** 34 * TODO: Merge change to add package prefix to tradefed's GTestResultParser. 35 * Otherwise this file should be the same as TradeFed's version. 36 * 37 * Parses the 'raw output mode' results of native tests using GTest that run from shell, and informs 38 * a ITestRunListener of the results. 39 * <p>Sample format of output expected: 40 * 41 * <pre> 42 * [==========] Running 15 tests from 1 test case. 43 * [----------] Global test environment set-up. 44 * [----------] 15 tests from MessageTest 45 * [ RUN ] MessageTest.DefaultConstructor 46 * [ OK ] MessageTest.DefaultConstructor (1 ms) 47 * [ RUN ] MessageTest.CopyConstructor 48 * external/gtest/test/gtest-message_test.cc:67: Failure 49 * Value of: 5 50 * Expected: 2 51 * external/gtest/test/gtest-message_test.cc:68: Failure 52 * Value of: 1 == 1 53 * Actual: true 54 * Expected: false 55 * [ FAILED ] MessageTest.CopyConstructor (2 ms) 56 * ... 57 * [ RUN ] MessageTest.DoesNotTakeUpMuchStackSpace 58 * [ OK ] MessageTest.DoesNotTakeUpMuchStackSpace (0 ms) 59 * [----------] 15 tests from MessageTest (26 ms total) 60 * 61 * [----------] Global test environment tear-down 62 * [==========] 15 tests from 1 test case ran. (26 ms total) 63 * [ PASSED ] 6 tests. 64 * [ FAILED ] 9 tests, listed below: 65 * [ FAILED ] MessageTest.CopyConstructor 66 * [ FAILED ] MessageTest.ConstructsFromCString 67 * [ FAILED ] MessageTest.StreamsCString 68 * [ FAILED ] MessageTest.StreamsNullCString 69 * [ FAILED ] MessageTest.StreamsString 70 * [ FAILED ] MessageTest.StreamsStringWithEmbeddedNUL 71 * [ FAILED ] MessageTest.StreamsNULChar 72 * [ FAILED ] MessageTest.StreamsInt 73 * [ FAILED ] MessageTest.StreamsBasicIoManip 74 * 9 FAILED TESTS 75 * </pre> 76 * 77 * <p>where the following tags are used to signal certain events: 78 * <pre> 79 * [==========]: the first occurrence indicates a new run started, including the number of tests 80 * to be expected in this run 81 * [ RUN ]: indicates a new test has started to run; a series of zero or more lines may 82 * follow a test start, and will be captured in case of a test failure or error 83 * [ OK ]: the preceding test has completed successfully, optionally including the time it 84 * took to run (in ms) 85 * [ FAILED ]: the preceding test has failed, optionally including the time it took to run (in ms) 86 * [==========]: the preceding test run has completed, optionally including the time it took to run 87 * (in ms) 88 * </pre> 89 * 90 * All other lines are ignored. 91 */ 92 public class GeeTestResultParser extends MultiLineReceiver { 93 private static final String LOG_TAG = "GTestResultParser"; 94 95 // Variables to keep track of state 96 private TestResult mCurrentTestResult = null; 97 private int mNumTestsRun = 0; 98 private int mNumTestsExpected = 0; 99 private long mTotalRunTime = 0; 100 private boolean mTestInProgress = false; 101 private boolean mTestRunInProgress = false; 102 private final String mTestRunId; 103 private final Collection<ITestRunListener> mTestListeners; 104 105 /** Fake adding a package prefix if the test listener needs it. */ 106 private String mFakePackagePrefix = ""; 107 108 /** True if start of test has already been reported to listener. */ 109 private boolean mTestRunStartReported = false; 110 111 /** True if current test run has been canceled by user. */ 112 private boolean mIsCancelled = false; 113 114 private String mCoverageTarget = null; 115 116 /** 117 * Test result data 118 */ 119 private static class TestResult { 120 private String mTestName = null; 121 private String mTestClass = null; 122 private StringBuilder mStackTrace = null; 123 @SuppressWarnings("unused") 124 private Long mRunTime = null; 125 126 /** Returns whether expected values have been parsed 127 * 128 * @return true if all expected values have been parsed 129 */ isComplete()130 boolean isComplete() { 131 return mTestName != null && mTestClass != null; 132 } 133 134 /** Returns whether there is currently a stack trace 135 * 136 * @return true if there is currently a stack trace, false otherwise 137 */ hasStackTrace()138 boolean hasStackTrace() { 139 return mStackTrace != null; 140 } 141 142 /** 143 * Returns the stack trace of the current test. 144 * 145 * @return a String representation of the current test's stack trace; if there is not 146 * a current stack trace, it returns an error string. Use {@link TestResult#hasStackTrace} 147 * if you need to know whether there is a stack trace. 148 */ getTrace()149 String getTrace() { 150 if (hasStackTrace()) { 151 return mStackTrace.toString(); 152 } else { 153 Log.e(LOG_TAG, "Could not find stack trace for failed test"); 154 return new Throwable("Unknown failure").toString(); 155 } 156 } 157 158 /** Provides a more user readable string for TestResult, if possible */ 159 @Override toString()160 public String toString() { 161 StringBuilder output = new StringBuilder(); 162 if (mTestClass != null ) { 163 output.append(mTestClass); 164 output.append('#'); 165 } 166 if (mTestName != null) { 167 output.append(mTestName); 168 } 169 if (output.length() > 0) { 170 return output.toString(); 171 } 172 return "unknown result"; 173 } 174 } 175 176 /** Internal helper struct to store parsed test info. */ 177 private static class ParsedTestInfo { 178 String mTestName = null; 179 String mTestClassName = null; 180 String mTestRunTime = null; 181 ParsedTestInfo(String testName, String testClassName, String testRunTime)182 public ParsedTestInfo(String testName, String testClassName, String testRunTime) { 183 mTestName = testName; 184 mTestClassName = testClassName; 185 mTestRunTime = testRunTime; 186 } 187 } 188 189 /** Prefixes used to demarcate and identify output. */ 190 private static class Prefixes { 191 @SuppressWarnings("unused") 192 private static final String INFORMATIONAL_MARKER = "[----------]"; 193 private static final String START_TEST_RUN_MARKER = "[==========] Running"; 194 private static final String TEST_RUN_MARKER = "[==========]"; 195 private static final String START_TEST_MARKER = "[ RUN ]"; 196 private static final String OK_TEST_MARKER = "[ OK ]"; 197 private static final String FAILED_TEST_MARKER = "[ FAILED ]"; 198 } 199 200 /** 201 * Creates the GTestResultParser. 202 * 203 * @param testRunId the test run id to provide to 204 * {@link ITestRunListener#testRunStarted(String, int)} 205 * @param listeners informed of test results as the tests are executing 206 */ GeeTestResultParser(String testRunId, Collection<ITestRunListener> listeners)207 public GeeTestResultParser(String testRunId, Collection<ITestRunListener> listeners) { 208 mTestRunId = testRunId; 209 mTestListeners = new ArrayList<ITestRunListener>(listeners); 210 } 211 212 /** 213 * Creates the GTestResultParser for a single listener. 214 * 215 * @param testRunId the test run id to provide to 216 * {@link ITestRunListener#testRunStarted(String, int)} 217 * @param listener informed of test results as the tests are executing 218 */ GeeTestResultParser(String testRunId, ITestRunListener listener)219 public GeeTestResultParser(String testRunId, ITestRunListener listener) { 220 mTestRunId = testRunId; 221 mTestListeners = new ArrayList<ITestRunListener>(1); 222 mTestListeners.add(listener); 223 } 224 225 /** 226 * Package prefix to be added to test names when they are reported like 227 * "android.nativemedia." You may need to add the dot if you need it. 228 */ setFakePackagePrefix(String prefix)229 public void setFakePackagePrefix(String prefix) { 230 mFakePackagePrefix = prefix; 231 } 232 233 /** 234 * Returns the current TestResult for test in progress, or a new default one. 235 * 236 * @return The TestResult for the current test run 237 */ getCurrentTestResult()238 private TestResult getCurrentTestResult() { 239 if (mCurrentTestResult == null) { 240 mCurrentTestResult = new TestResult(); 241 } 242 return mCurrentTestResult; 243 } 244 245 246 /** 247 * Clears out the current TestResult. 248 */ clearCurrentTestResult()249 private void clearCurrentTestResult() { 250 mCurrentTestResult = null; 251 } 252 253 /** 254 * {@inheritDoc} 255 */ 256 @Override processNewLines(String[] lines)257 public void processNewLines(String[] lines) { 258 for (String line : lines) { 259 parse(line); 260 } 261 } 262 263 /** 264 * Parse an individual output line. 265 * 266 * @param line Text output line 267 */ parse(String line)268 private void parse(String line) { 269 String message = null; 270 271 if (mTestRunInProgress || line.startsWith(Prefixes.TEST_RUN_MARKER)) { 272 if (line.startsWith(Prefixes.START_TEST_MARKER)) { 273 // Individual test started 274 message = line.substring(Prefixes.START_TEST_MARKER.length()).trim(); 275 processTestStartedTag(message); 276 } 277 else if (line.startsWith(Prefixes.OK_TEST_MARKER)) { 278 // Individual test completed successfully 279 message = line.substring(Prefixes.OK_TEST_MARKER.length()).trim(); 280 processOKTag(message); 281 clearCurrentTestResult(); 282 } 283 else if (line.startsWith(Prefixes.FAILED_TEST_MARKER)) { 284 // Individual test completed with failure 285 message = line.substring(Prefixes.FAILED_TEST_MARKER.length()).trim(); 286 processFailedTag(message); 287 clearCurrentTestResult(); 288 } 289 else if (line.startsWith(Prefixes.START_TEST_RUN_MARKER)) { 290 // Test run started 291 // Make sure to leave the "Running" in the string 292 message = line.substring(Prefixes.TEST_RUN_MARKER.length()).trim(); 293 processRunStartedTag(message); 294 } 295 else if (line.startsWith(Prefixes.TEST_RUN_MARKER)) { 296 // Test run ended 297 // This is for the end of the test suite run, so make sure this else-if is after the 298 // check for START_TEST_SUITE_MARKER 299 message = line.substring(Prefixes.TEST_RUN_MARKER.length()).trim(); 300 processRunCompletedTag(message); 301 } 302 else if (testInProgress()) { 303 // Note this does not handle the case of an error outside an actual test run 304 appendTestOutputLine(line); 305 } 306 } 307 } 308 309 /** 310 * Returns true if test run canceled. 311 * 312 * @see IShellOutputReceiver#isCancelled() 313 */ 314 @Override isCancelled()315 public boolean isCancelled() { 316 return mIsCancelled; 317 } 318 319 /** 320 * Requests cancellation of test run. 321 */ cancel()322 public void cancel() { 323 mIsCancelled = true; 324 } 325 326 /** 327 * Returns whether we're in the middle of running a test. 328 * 329 * @return True if a test was started, false otherwise 330 */ testInProgress()331 private boolean testInProgress() { 332 return mTestInProgress; 333 } 334 335 /** 336 * Set state to indicate we've started running a test. 337 * 338 */ setTestStarted()339 private void setTestStarted() { 340 mTestInProgress = true; 341 } 342 343 /** 344 * Set state to indicate we've started running a test. 345 * 346 */ setTestEnded()347 private void setTestEnded() { 348 mTestInProgress = false; 349 } 350 351 /** 352 * Reports the start of a test run, and the total test count, if it has not been previously 353 * reported. 354 */ reportTestRunStarted()355 private void reportTestRunStarted() { 356 // if start test run not reported yet 357 if (!mTestRunStartReported) { 358 for (ITestRunListener listener : mTestListeners) { 359 listener.testRunStarted(mTestRunId, mNumTestsExpected); 360 } 361 mTestRunStartReported = true; 362 } 363 } 364 365 /** 366 * Reports the end of a test run, and resets that test 367 */ reportTestRunEnded()368 private void reportTestRunEnded() { 369 for (ITestRunListener listener : mTestListeners) { 370 listener.testRunEnded(mTotalRunTime, getRunMetrics()); 371 } 372 mTestRunStartReported = false; 373 } 374 375 /** 376 * Create the run metrics {@link Map} to report. 377 * 378 * @return a {@link Map} of run metrics data 379 */ getRunMetrics()380 private Map<String, String> getRunMetrics() { 381 Map<String, String> metricsMap = new HashMap<String, String>(); 382 if (mCoverageTarget != null) { 383 metricsMap.put(XmlDefsTest.COVERAGE_TARGET_KEY, mCoverageTarget); 384 } 385 return metricsMap; 386 } 387 388 /** 389 * Parse the test identifier (class and test name), and optional time info. 390 * 391 * @param identifier Raw identifier of the form classname.testname, with an optional time 392 * element in the format of (XX ms) at the end 393 * @return A ParsedTestInfo representing the parsed info from the identifier string. 394 * 395 * If no time tag was detected, then the third element in the array (time_in_ms) will 396 * be null. If the line failed to parse properly (eg: could not determine name of 397 * test/class) then an "UNKNOWN" string value will be returned for the classname and 398 * testname. This method guarantees a string will always be returned for the class and 399 * test names (but not for the time value). 400 */ parseTestIdentifier(String identifier)401 private ParsedTestInfo parseTestIdentifier(String identifier) { 402 ParsedTestInfo returnInfo = new ParsedTestInfo("UNKNOWN_CLASS", "UNKNOWN_TEST", null); 403 404 Pattern timePattern = Pattern.compile(".*(\\((\\d+) ms\\))"); // eg: (XX ms) 405 Matcher time = timePattern.matcher(identifier); 406 407 // Try to find a time 408 if (time.find()) { 409 String timeString = time.group(2); // the "XX" in "(XX ms)" 410 String discardPortion = time.group(1); // everything after the test class/name 411 identifier = identifier.substring(0, identifier.lastIndexOf(discardPortion)).trim(); 412 returnInfo.mTestRunTime = timeString; 413 } 414 415 String[] testId = identifier.split("\\."); 416 if (testId.length < 2) { 417 Log.e(LOG_TAG, "Could not detect the test class and test name, received: " + 418 identifier); 419 } 420 else { 421 returnInfo.mTestClassName = testId[0]; 422 returnInfo.mTestName = testId[1]; 423 } 424 return returnInfo; 425 } 426 427 /** 428 * Parses and stores the test identifier (class and test name). 429 * 430 * @param identifier Raw identifier 431 */ processRunStartedTag(String identifier)432 private void processRunStartedTag(String identifier) { 433 // eg: (Running XX tests from 1 test case.) 434 Pattern numTestsPattern = Pattern.compile("Running (\\d+) test[s]? from .*"); 435 Matcher numTests = numTestsPattern.matcher(identifier); 436 437 // Try to find number of tests 438 if (numTests.find()) { 439 try { 440 mNumTestsExpected = Integer.parseInt(numTests.group(1)); 441 } 442 catch (NumberFormatException e) { 443 Log.e(LOG_TAG, "Unable to determine number of tests expected, received: " + 444 numTests.group(1)); 445 } 446 } 447 if (mNumTestsExpected > 0) { 448 reportTestRunStarted(); 449 mNumTestsRun = 0; 450 mTestRunInProgress = true; 451 } 452 } 453 454 /** 455 * Processes and informs listener when we encounter a tag indicating that a test suite is done. 456 * 457 * @param identifier Raw log output from the suite ended tag 458 */ processRunCompletedTag(String identifier)459 private void processRunCompletedTag(String identifier) { 460 Pattern timePattern = Pattern.compile(".*\\((\\d+) ms total\\)"); // eg: (XX ms total) 461 Matcher time = timePattern.matcher(identifier); 462 463 // Try to find the total run time 464 if (time.find()) { 465 try { 466 mTotalRunTime = Long.parseLong(time.group(1)); 467 } 468 catch (NumberFormatException e) { 469 Log.e(LOG_TAG, "Unable to determine the total running time, received: " + 470 time.group(1)); 471 } 472 } 473 reportTestRunEnded(); 474 mTestRunInProgress = false; 475 } 476 477 /** 478 * Processes and informs listener when we encounter a tag indicating that a test has started. 479 * 480 * @param identifier Raw log output of the form classname.testname, with an optional time (x ms) 481 */ processTestStartedTag(String identifier)482 private void processTestStartedTag(String identifier) { 483 ParsedTestInfo parsedResults = parseTestIdentifier(identifier); 484 TestResult testResult = getCurrentTestResult(); 485 testResult.mTestClass = parsedResults.mTestClassName; 486 testResult.mTestName = parsedResults.mTestName; 487 488 TestIdentifier testId = new TestIdentifier(mFakePackagePrefix + testResult.mTestClass, 489 testResult.mTestName); 490 491 for (ITestRunListener listener : mTestListeners) { 492 listener.testStarted(testId); 493 } 494 setTestStarted(); 495 } 496 497 /** 498 * Helper method to do the work necessary when a test has ended. 499 * 500 * @param identifier Raw log output of the form "classname.testname" with an optional (XX ms) 501 * at the end indicating the running time. 502 * @param testPassed Indicates whether the test passed or failed (set to true if passed, false 503 * if failed) 504 */ doTestEnded(String identifier, boolean testPassed)505 private void doTestEnded(String identifier, boolean testPassed) { 506 ParsedTestInfo parsedResults = parseTestIdentifier(identifier); 507 TestResult testResult = getCurrentTestResult(); 508 TestIdentifier testId = new TestIdentifier(mFakePackagePrefix + testResult.mTestClass, 509 testResult.mTestName); 510 511 // Error - trying to end a test when one isn't in progress 512 if (!testInProgress()) { 513 Log.e(LOG_TAG, "Test currently not in progress when trying to end test: " + identifier); 514 return; 515 } 516 517 // Save the run time for this test if one exists 518 if (parsedResults.mTestRunTime != null) { 519 try { 520 testResult.mRunTime = new Long(parsedResults.mTestRunTime); 521 } 522 catch (NumberFormatException e) { 523 Log.e(LOG_TAG, "Test run time value is invalid, received: " + 524 parsedResults.mTestRunTime); 525 } 526 } 527 528 // Check that the test result is for the same test/class we're expecting it to be for 529 boolean encounteredUnexpectedTest = false; 530 if (!testResult.isComplete()) { 531 Log.e(LOG_TAG, "No test/class name is currently recorded as running!"); 532 } 533 else { 534 if (testResult.mTestClass.compareTo(parsedResults.mTestClassName) != 0) { 535 Log.e(LOG_TAG, "Name for current test class does not match class we started " + 536 "with, expected: " + testResult.mTestClass + " but got: " + 537 parsedResults.mTestClassName); 538 encounteredUnexpectedTest = true; 539 } 540 if (testResult.mTestName.compareTo(parsedResults.mTestName) != 0) { 541 Log.e(LOG_TAG, "Name for current test does not match test we started with," + 542 "expected: " + testResult.mTestName + " bug got: " + 543 parsedResults.mTestName); 544 encounteredUnexpectedTest = true; 545 } 546 } 547 548 if (encounteredUnexpectedTest) { 549 // If the test name of the result changed from what we started with, report that 550 // the last known test failed, regardless of whether we received a pass or fail tag. 551 for (ITestRunListener listener : mTestListeners) { 552 listener.testFailed(testId, mCurrentTestResult.getTrace()); 553 } 554 } 555 else if (!testPassed) { // test failed 556 for (ITestRunListener listener : mTestListeners) { 557 listener.testFailed(testId, mCurrentTestResult.getTrace()); 558 } 559 } 560 // For all cases (pass or fail), we ultimately need to report test has ended 561 Map <String, String> emptyMap = Collections.emptyMap(); 562 for (ITestRunListener listener : mTestListeners) { 563 // @TODO: Add reporting of test run time to ITestRunListener 564 listener.testEnded(testId, emptyMap); 565 } 566 567 setTestEnded(); 568 ++mNumTestsRun; 569 } 570 571 /** 572 * Processes and informs listener when we encounter the OK tag. 573 * 574 * @param identifier Raw log output of the form "classname.testname" with an optional (XX ms) 575 * at the end indicating the running time. 576 */ processOKTag(String identifier)577 private void processOKTag(String identifier) { 578 doTestEnded(identifier, true); 579 } 580 581 /** 582 * Processes and informs listener when we encounter the FAILED tag. 583 * 584 * @param identifier Raw log output of the form "classname.testname" with an optional (XX ms) 585 * at the end indicating the running time. 586 */ processFailedTag(String identifier)587 private void processFailedTag(String identifier) { 588 doTestEnded(identifier, false); 589 } 590 591 /** 592 * Appends the test output to the current TestResult. 593 * 594 * @param line Raw test result line of output. 595 */ appendTestOutputLine(String line)596 private void appendTestOutputLine(String line) { 597 TestResult testResult = getCurrentTestResult(); 598 if (testResult.mStackTrace == null) { 599 testResult.mStackTrace = new StringBuilder(); 600 } 601 else { 602 testResult.mStackTrace.append("\r\n"); 603 } 604 testResult.mStackTrace.append(line); 605 } 606 607 /** 608 * Process an instrumentation run failure 609 * 610 * @param errorMsg The message to output about the nature of the error 611 */ handleTestRunFailed(String errorMsg)612 private void handleTestRunFailed(String errorMsg) { 613 errorMsg = (errorMsg == null ? "Unknown error" : errorMsg); 614 Log.i(LOG_TAG, String.format("Test run failed: %s", errorMsg)); 615 String testRunStackTrace = ""; 616 617 // Report that the last known test failed 618 if ((mCurrentTestResult != null) && (mCurrentTestResult.isComplete())) { 619 // current test results are cleared out after every complete test run, 620 // if it's not null, assume the last test caused this and report as a test failure 621 TestIdentifier testId = new TestIdentifier(mCurrentTestResult.mTestClass, 622 mCurrentTestResult.mTestName); 623 624 // If there was any stack trace during the test run, append it to the "test failed" 625 // error message so we have an idea of what caused the crash/failure. 626 Map<String, String> emptyMap = Collections.emptyMap(); 627 if (mCurrentTestResult.hasStackTrace()) { 628 testRunStackTrace = mCurrentTestResult.getTrace(); 629 } 630 for (ITestRunListener listener : mTestListeners) { 631 listener.testFailed(testId, "No test results.\r\n" + testRunStackTrace); 632 listener.testEnded(testId, emptyMap); 633 } 634 clearCurrentTestResult(); 635 } 636 // Report the test run failed 637 for (ITestRunListener listener : mTestListeners) { 638 listener.testRunFailed(errorMsg); 639 listener.testRunEnded(mTotalRunTime, getRunMetrics()); 640 } 641 } 642 643 /** 644 * Called by parent when adb session is complete. 645 */ 646 @Override done()647 public void done() { 648 super.done(); 649 if (mNumTestsExpected > mNumTestsRun) { 650 handleTestRunFailed(String.format("Test run incomplete. Expected %d tests, received %d", 651 mNumTestsExpected, mNumTestsRun)); 652 } 653 else if (mTestRunInProgress) { 654 handleTestRunFailed("No test results"); 655 } 656 } 657 658 /** 659 * Sets the coverage target for this test. 660 * <p/> 661 * Will be sent as a metric to test listeners. 662 * 663 * @param coverageTarget the coverage target 664 */ setCoverageTarget(String coverageTarget)665 public void setCoverageTarget(String coverageTarget) { 666 mCoverageTarget = coverageTarget; 667 } 668 } 669