1 /* 2 * Copyright (C) 2016 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.tradefed.testtype; 17 18 import com.android.ddmlib.MultiLineReceiver; 19 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric; 20 import com.android.tradefed.result.ITestInvocationListener; 21 import com.android.tradefed.result.TestDescription; 22 23 import java.util.Collection; 24 import java.util.HashMap; 25 import java.util.Map; 26 import java.util.Map.Entry; 27 import java.util.regex.Matcher; 28 import java.util.regex.Pattern; 29 30 /** 31 * Interprets the output of tests run with Python's unittest framework and translates it into calls 32 * on a series of {@link ITestInvocationListener}s. Output from these tests follows this EBNF 33 * grammar: 34 * 35 * <p>TestReport ::= TestResult* Line TimeMetric [FailMessage*] Status. TestResult ::= string 36 * (string) SingleStatus. FailMessage ::= EqLine ERROR: string (string) Line Traceback 37 * Line. SingleStatus ::= ok | ERROR. TimeMetric ::= Ran integer tests in float s. Status 38 * ::= OK | FAILED (errors= int ). Traceback ::= string+. 39 * 40 * <p>Example output (passing): test_size (test_rangelib.RangeSetTest) ... ok test_str 41 * (test_rangelib.RangeSetTest) ... ok test_subtract (test_rangelib.RangeSetTest) ... ok 42 * test_to_string_raw (test_rangelib.RangeSetTest) ... ok test_union (test_rangelib.RangeSetTest) 43 * ... ok 44 * 45 * <p>---------------------------------------------------------------------- Ran 5 tests in 0.002s 46 * 47 * <p>OK 48 * 49 * <p>Example output (failed) test_size (test_rangelib.RangeSetTest) ... ERROR 50 * 51 * <p>====================================================================== ERROR: test_size 52 * (test_rangelib.RangeSetTest) 53 * ---------------------------------------------------------------------- Traceback (most recent 54 * call last): File "test_rangelib.py", line 129, in test_rangelib raise ValueError() ValueError 55 * ---------------------------------------------------------------------- Ran 1 test in 0.001s 56 * FAILED (errors=1) 57 * 58 * <p>Example output with several edge cases (failed): testError (foo.testFoo) ... ERROR 59 * testExpectedFailure (foo.testFoo) ... expected failure testFail (foo.testFoo) ... FAIL 60 * testFailWithDocString (foo.testFoo) foo bar ... FAIL testOk (foo.testFoo) ... ok 61 * testOkWithDocString (foo.testFoo) foo bar ... ok testSkipped (foo.testFoo) ... skipped 'reason 62 * foo' testUnexpectedSuccess (foo.testFoo) ... unexpected success 63 * 64 * <p>====================================================================== ERROR: testError 65 * (foo.testFoo) ---------------------------------------------------------------------- Traceback 66 * (most recent call last): File "foo.py", line 11, in testError self.assertEqual(2+2, 5/0) 67 * ZeroDivisionError: integer division or modulo by zero 68 * 69 * <p>====================================================================== FAIL: testFail 70 * (foo.testFoo) ---------------------------------------------------------------------- Traceback 71 * (most recent call last): File "foo.py", line 8, in testFail self.assertEqual(2+2, 5) 72 * AssertionError: 4 != 5 73 * 74 * <p>====================================================================== FAIL: 75 * testFailWithDocString (foo.testFoo) foo bar 76 * ---------------------------------------------------------------------- Traceback (most recent 77 * call last): File "foo.py", line 31, in testFailWithDocString self.assertEqual(2+2, 5) 78 * AssertionError: 4 != 5 79 * 80 * <p>---------------------------------------------------------------------- Ran 8 tests in 0.001s 81 * 82 * <p>FAILED (failures=2, errors=1, skipped=1, expected failures=1, unexpected successes=1) 83 */ 84 public class PythonUnitTestResultParser extends MultiLineReceiver { 85 86 // Current test state 87 private ParserState mCurrentParseState; 88 private String mCurrentTestName; 89 private String mCurrentTestClass; 90 private String mCurrentTestStatus; 91 private Matcher mCurrentMatcher; 92 private StringBuilder mCurrentTraceback; 93 private long mTotalElapsedTime; 94 private int mTotalTestCount; 95 private int mFailedTestCount; 96 97 // General state 98 private final Collection<ITestInvocationListener> mListeners; 99 private final String mRunName; 100 private Map<TestDescription, String> mTestResultCache; 101 // Use a special entry to mark skipped test in mTestResultCache 102 static final String SKIPPED_ENTRY = "Skipped"; 103 104 // Constant tokens that appear in the result grammar. 105 static final String EQUAL_LINE = 106 "======================================================================"; 107 static final String DASH_LINE = 108 "----------------------------------------------------------------------"; 109 static final String TRACEBACK_LINE = 110 "Traceback (most recent call last):"; 111 112 static final Pattern PATTERN_TEST_SUCCESS = Pattern.compile("ok|expected failure"); 113 static final Pattern PATTERN_TEST_FAILURE = Pattern.compile("FAIL|ERROR"); 114 static final Pattern PATTERN_TEST_SKIPPED = Pattern.compile("skipped '.*"); 115 static final Pattern PATTERN_TEST_UNEXPECTED_SUCCESS = Pattern.compile("unexpected success"); 116 117 static final Pattern PATTERN_ONE_LINE_RESULT = Pattern.compile( 118 "(\\S*) \\((\\S*)\\) ... (ok|expected failure|FAIL|ERROR|skipped '.*'|unexpected success)"); 119 static final Pattern PATTERN_TWO_LINE_RESULT_FIRST = Pattern.compile( 120 "(\\S*) \\((\\S*)\\)"); 121 static final Pattern PATTERN_TWO_LINE_RESULT_SECOND = Pattern.compile( 122 "(.*) ... (ok|expected failure|FAIL|ERROR|skipped '.*'|unexpected success)"); 123 static final Pattern PATTERN_FAIL_MESSAGE = Pattern.compile( 124 "(FAIL|ERROR): (\\S*) \\((\\S*)\\)"); 125 static final Pattern PATTERN_RUN_SUMMARY = Pattern.compile( 126 "Ran (\\d+) tests? in (\\d+(.\\d*)?)s"); 127 128 static final Pattern PATTERN_RUN_RESULT = Pattern.compile("(OK|FAILED).*"); 129 130 enum ParserState { 131 TEST_CASE, 132 FAIL_MESSAGE, 133 TRACEBACK, 134 SUMMARY, 135 COMPLETE, 136 } 137 138 private class PythonUnitTestParseException extends Exception { 139 static final long serialVersionUID = -3387516993124229948L; 140 141 public PythonUnitTestParseException(String reason) { 142 super(reason); 143 } 144 } 145 146 /** 147 * Create a new {@link PythonUnitTestResultParser} that reports to the given {@link 148 * ITestInvocationListener}s. 149 */ 150 public PythonUnitTestResultParser( 151 Collection<ITestInvocationListener> listeners, String runName) { 152 mListeners = listeners; 153 mRunName = runName; 154 mTestResultCache = new HashMap<>(); 155 } 156 157 /** 158 * Process Python unittest output and report parsed results. 159 * 160 * <p>This method should be called only once with the full output, unlike the base method in 161 * {@link MultiLineReceiver}. 162 */ 163 @Override 164 public void processNewLines(String[] lines) { 165 try { 166 if (lines.length < 1 || isTracebackLine(lines[0])) { 167 throw new PythonUnitTestParseException("Test execution failed"); 168 } 169 170 mCurrentParseState = ParserState.TEST_CASE; 171 for (String line : lines) { 172 parse(line); 173 } 174 175 if (mCurrentParseState != ParserState.COMPLETE) { 176 throw new PythonUnitTestParseException( 177 "Parser finished in unexpected state " + mCurrentParseState.toString()); 178 } 179 } catch (PythonUnitTestParseException e) { 180 throw new RuntimeException("Failed to parse Python unittest result", e); 181 } 182 } 183 184 /** Parse the next result line according to current parser state. */ 185 void parse(String line) throws PythonUnitTestParseException { 186 switch (mCurrentParseState) { 187 case TEST_CASE: 188 processTestCase(line); 189 break; 190 case TRACEBACK: 191 processTraceback(line); 192 break; 193 case SUMMARY: 194 processRunSummary(line); 195 break; 196 case FAIL_MESSAGE: 197 processFailMessage(line); 198 break; 199 case COMPLETE: 200 break; 201 } 202 } 203 204 /** Process a test case line and collect the test name, class, and status. */ 205 void processTestCase(String line) throws PythonUnitTestParseException { 206 if (isEqualLine(line)) { 207 // equal line before fail message 208 mCurrentParseState = ParserState.FAIL_MESSAGE; 209 } else if (isDashLine(line)) { 210 // dash line before run summary 211 mCurrentParseState = ParserState.SUMMARY; 212 } else if (lineMatchesPattern(line, PATTERN_ONE_LINE_RESULT)) { 213 mCurrentTestName = mCurrentMatcher.group(1); 214 mCurrentTestClass = mCurrentMatcher.group(2); 215 mCurrentTestStatus = mCurrentMatcher.group(3); 216 reportNonFailureTestResult(); 217 } else if (lineMatchesPattern(line, PATTERN_TWO_LINE_RESULT_FIRST)) { 218 mCurrentTestName = mCurrentMatcher.group(1); 219 mCurrentTestClass = mCurrentMatcher.group(2); 220 } else if (lineMatchesPattern(line, PATTERN_TWO_LINE_RESULT_SECOND)) { 221 mCurrentTestStatus = mCurrentMatcher.group(2); 222 reportNonFailureTestResult(); 223 } 224 } 225 226 /** Process a fail message line and collect the test name, class, and status. */ 227 void processFailMessage(String line) { 228 if (isDashLine(line)) { 229 // dash line before traceback 230 mCurrentParseState = ParserState.TRACEBACK; 231 mCurrentTraceback = new StringBuilder(); 232 } else if (lineMatchesPattern(line, PATTERN_FAIL_MESSAGE)) { 233 mCurrentTestName = mCurrentMatcher.group(2); 234 mCurrentTestClass = mCurrentMatcher.group(3); 235 mCurrentTestStatus = mCurrentMatcher.group(1); 236 } 237 // optional docstring - do nothing 238 } 239 240 /** Process a traceback line and append it to the full traceback message. */ 241 void processTraceback(String line) { 242 if (isDashLine(line)) { 243 // dash line before run summary 244 mCurrentParseState = ParserState.SUMMARY; 245 reportFailureTestResult(); 246 } else if (isEqualLine(line)) { 247 // equal line before another fail message followed by traceback 248 mCurrentParseState = ParserState.FAIL_MESSAGE; 249 reportFailureTestResult(); 250 } else { 251 if (mCurrentTraceback.length() > 0) { 252 mCurrentTraceback.append(System.lineSeparator()); 253 } 254 mCurrentTraceback.append(line); 255 } 256 } 257 258 /** Process the run summary line and collect the test count and run time. */ 259 void processRunSummary(String line) { 260 if (lineMatchesPattern(line, PATTERN_RUN_SUMMARY)) { 261 mTotalTestCount = Integer.parseInt(mCurrentMatcher.group(1)); 262 double timeInSeconds = Double.parseDouble(mCurrentMatcher.group(2)); 263 mTotalElapsedTime = (long) timeInSeconds * 1000; 264 reportToListeners(); 265 mCurrentParseState = ParserState.COMPLETE; 266 } 267 // ignore status message on the last line because Python consider "unexpected success" 268 // passed while we consider it failed 269 } 270 271 boolean isEqualLine(String line) { 272 return line.startsWith(EQUAL_LINE); 273 } 274 275 boolean isDashLine(String line) { 276 return line.startsWith(DASH_LINE); 277 } 278 279 boolean isTracebackLine(String line) { 280 return line.startsWith(TRACEBACK_LINE); 281 } 282 283 /** Check if the given line matches the given pattern and caches the matcher object */ 284 private boolean lineMatchesPattern(String line, Pattern p) { 285 mCurrentMatcher = p.matcher(line); 286 return mCurrentMatcher.matches(); 287 } 288 289 /** Send recorded test results to all listeners. */ 290 private void reportToListeners() { 291 String failReason = String.format("Failed %d tests", mFailedTestCount); 292 for (ITestInvocationListener listener : mListeners) { 293 listener.testRunStarted(mRunName, mTotalTestCount); 294 295 for (Entry<TestDescription, String> test : mTestResultCache.entrySet()) { 296 listener.testStarted(test.getKey()); 297 if (SKIPPED_ENTRY.equals(test.getValue())) { 298 listener.testIgnored(test.getKey()); 299 } else if (test.getValue() != null) { 300 listener.testFailed(test.getKey(), test.getValue()); 301 } 302 listener.testEnded(test.getKey(), new HashMap<String, Metric>()); 303 } 304 305 if (mFailedTestCount > 0) { 306 listener.testRunFailed(failReason); 307 } 308 listener.testRunEnded(mTotalElapsedTime, new HashMap<String, Metric>()); 309 } 310 311 } 312 313 /** Record a non-failure test case. */ 314 private void reportNonFailureTestResult() throws PythonUnitTestParseException { 315 TestDescription testId = new TestDescription(mCurrentTestClass, mCurrentTestName); 316 if (PATTERN_TEST_SUCCESS.matcher(mCurrentTestStatus).matches()) { 317 mTestResultCache.put(testId, null); 318 } else if (PATTERN_TEST_SKIPPED.matcher(mCurrentTestStatus).matches()) { 319 mTestResultCache.put(testId, SKIPPED_ENTRY); 320 } else if (PATTERN_TEST_UNEXPECTED_SUCCESS.matcher(mCurrentTestStatus).matches()) { 321 mTestResultCache.put(testId, "Test unexpected succeeded"); 322 mFailedTestCount++; 323 } else if (PATTERN_TEST_FAILURE.matcher(mCurrentTestStatus).matches()) { 324 // do nothing for now, report only after traceback is collected 325 } else { 326 throw new PythonUnitTestParseException("Unrecognized test status"); 327 } 328 } 329 330 /** Record a failed test case and its traceback message. */ 331 private void reportFailureTestResult() { 332 TestDescription testId = new TestDescription(mCurrentTestClass, mCurrentTestName); 333 mTestResultCache.put(testId, mCurrentTraceback.toString()); 334 mFailedTestCount++; 335 } 336 337 @Override 338 public boolean isCancelled() { 339 return false; 340 } 341 } 342