1#  Copyright (c) 2011-present, Facebook, Inc.  All rights reserved.
2#  This source code is licensed under both the GPLv2 (found in the
3#  COPYING file in the root directory) and Apache 2.0 License
4#  (found in the LICENSE.Apache file in the root directory).
5
6'''Filter for error messages in test output:
7    - Receives merged stdout/stderr from test on stdin
8    - Finds patterns of known error messages for test name (first argument)
9    - Prints those error messages to stdout
10'''
11
12from __future__ import absolute_import
13from __future__ import division
14from __future__ import print_function
15from __future__ import unicode_literals
16
17import re
18import sys
19
20
21class ErrorParserBase(object):
22    def parse_error(self, line):
23        '''Parses a line of test output. If it contains an error, returns a
24        formatted message describing the error; otherwise, returns None.
25        Subclasses must override this method.
26        '''
27        raise NotImplementedError
28
29
30class GTestErrorParser(ErrorParserBase):
31    '''A parser that remembers the last test that began running so it can print
32    that test's name upon detecting failure.
33    '''
34    _GTEST_NAME_PATTERN = re.compile(r'\[ RUN      \] (\S+)$')
35    # format: '<filename or "unknown file">:<line #>: Failure'
36    _GTEST_FAIL_PATTERN = re.compile(r'(unknown file|\S+:\d+): Failure$')
37
38    def __init__(self):
39        self._last_gtest_name = 'Unknown test'
40
41    def parse_error(self, line):
42        gtest_name_match = self._GTEST_NAME_PATTERN.match(line)
43        if gtest_name_match:
44            self._last_gtest_name = gtest_name_match.group(1)
45            return None
46        gtest_fail_match = self._GTEST_FAIL_PATTERN.match(line)
47        if gtest_fail_match:
48            return '%s failed: %s' % (
49                    self._last_gtest_name, gtest_fail_match.group(1))
50        return None
51
52
53class MatchErrorParser(ErrorParserBase):
54    '''A simple parser that returns the whole line if it matches the pattern.
55    '''
56    def __init__(self, pattern):
57        self._pattern = re.compile(pattern)
58
59    def parse_error(self, line):
60        if self._pattern.match(line):
61            return line
62        return None
63
64
65class CompilerErrorParser(MatchErrorParser):
66    def __init__(self):
67        # format (compile error):
68        #   '<filename>:<line #>:<column #>: error: <error msg>'
69        # format (link error):
70        #   '<filename>:<line #>: error: <error msg>'
71        # The below regex catches both
72        super(CompilerErrorParser, self).__init__(r'\S+:\d+: error:')
73
74
75class ScanBuildErrorParser(MatchErrorParser):
76    def __init__(self):
77        super(ScanBuildErrorParser, self).__init__(
78                r'scan-build: \d+ bugs found.$')
79
80
81class DbCrashErrorParser(MatchErrorParser):
82    def __init__(self):
83        super(DbCrashErrorParser, self).__init__(r'\*\*\*.*\^$|TEST FAILED.')
84
85
86class WriteStressErrorParser(MatchErrorParser):
87    def __init__(self):
88        super(WriteStressErrorParser, self).__init__(
89                r'ERROR: write_stress died with exitcode=\d+')
90
91
92class AsanErrorParser(MatchErrorParser):
93    def __init__(self):
94        super(AsanErrorParser, self).__init__(
95                r'==\d+==ERROR: AddressSanitizer:')
96
97
98class UbsanErrorParser(MatchErrorParser):
99    def __init__(self):
100        # format: '<filename>:<line #>:<column #>: runtime error: <error msg>'
101        super(UbsanErrorParser, self).__init__(r'\S+:\d+:\d+: runtime error:')
102
103
104class ValgrindErrorParser(MatchErrorParser):
105    def __init__(self):
106        # just grab the summary, valgrind doesn't clearly distinguish errors
107        # from other log messages.
108        super(ValgrindErrorParser, self).__init__(r'==\d+== ERROR SUMMARY:')
109
110
111class CompatErrorParser(MatchErrorParser):
112    def __init__(self):
113        super(CompatErrorParser, self).__init__(r'==== .*[Ee]rror.* ====$')
114
115
116class TsanErrorParser(MatchErrorParser):
117    def __init__(self):
118        super(TsanErrorParser, self).__init__(r'WARNING: ThreadSanitizer:')
119
120
121_TEST_NAME_TO_PARSERS = {
122    'punit': [CompilerErrorParser, GTestErrorParser],
123    'unit': [CompilerErrorParser, GTestErrorParser],
124    'release': [CompilerErrorParser, GTestErrorParser],
125    'unit_481': [CompilerErrorParser, GTestErrorParser],
126    'release_481': [CompilerErrorParser, GTestErrorParser],
127    'clang_unit': [CompilerErrorParser, GTestErrorParser],
128    'clang_release': [CompilerErrorParser, GTestErrorParser],
129    'clang_analyze': [CompilerErrorParser, ScanBuildErrorParser],
130    'code_cov': [CompilerErrorParser, GTestErrorParser],
131    'unity': [CompilerErrorParser, GTestErrorParser],
132    'lite': [CompilerErrorParser],
133    'lite_test': [CompilerErrorParser, GTestErrorParser],
134    'stress_crash': [CompilerErrorParser, DbCrashErrorParser],
135    'stress_crash_with_atomic_flush': [CompilerErrorParser, DbCrashErrorParser],
136    'stress_crash_with_txn': [CompilerErrorParser, DbCrashErrorParser],
137    'write_stress': [CompilerErrorParser, WriteStressErrorParser],
138    'asan': [CompilerErrorParser, GTestErrorParser, AsanErrorParser],
139    'asan_crash': [CompilerErrorParser, AsanErrorParser, DbCrashErrorParser],
140    'asan_crash_with_atomic_flush': [CompilerErrorParser, AsanErrorParser, DbCrashErrorParser],
141    'asan_crash_with_txn': [CompilerErrorParser, AsanErrorParser, DbCrashErrorParser],
142    'ubsan': [CompilerErrorParser, GTestErrorParser, UbsanErrorParser],
143    'ubsan_crash': [CompilerErrorParser, UbsanErrorParser, DbCrashErrorParser],
144    'ubsan_crash_with_atomic_flush': [CompilerErrorParser, UbsanErrorParser, DbCrashErrorParser],
145    'ubsan_crash_with_txn': [CompilerErrorParser, UbsanErrorParser, DbCrashErrorParser],
146    'valgrind': [CompilerErrorParser, GTestErrorParser, ValgrindErrorParser],
147    'tsan': [CompilerErrorParser, GTestErrorParser, TsanErrorParser],
148    'format_compatible': [CompilerErrorParser, CompatErrorParser],
149    'run_format_compatible': [CompilerErrorParser, CompatErrorParser],
150    'no_compression': [CompilerErrorParser, GTestErrorParser],
151    'run_no_compression': [CompilerErrorParser, GTestErrorParser],
152    'regression': [CompilerErrorParser],
153    'run_regression': [CompilerErrorParser],
154}
155
156
157def main():
158    if len(sys.argv) != 2:
159        return 'Usage: %s <test name>' % sys.argv[0]
160    test_name = sys.argv[1]
161    if test_name not in _TEST_NAME_TO_PARSERS:
162        return 'Unknown test name: %s' % test_name
163
164    error_parsers = []
165    for parser_cls in _TEST_NAME_TO_PARSERS[test_name]:
166        error_parsers.append(parser_cls())
167
168    for line in sys.stdin:
169        line = line.strip()
170        for error_parser in error_parsers:
171            error_msg = error_parser.parse_error(line)
172            if error_msg is not None:
173                print(error_msg)
174
175
176if __name__ == '__main__':
177    sys.exit(main())
178