Home | History | Annotate | Download | only in testtype
      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