diff options
Diffstat (limited to 'pw_presubmit/py/pw_presubmit/ninja_parser.py')
-rw-r--r-- | pw_presubmit/py/pw_presubmit/ninja_parser.py | 115 |
1 files changed, 88 insertions, 27 deletions
diff --git a/pw_presubmit/py/pw_presubmit/ninja_parser.py b/pw_presubmit/py/pw_presubmit/ninja_parser.py index 131137df6..487af1754 100644 --- a/pw_presubmit/py/pw_presubmit/ninja_parser.py +++ b/pw_presubmit/py/pw_presubmit/ninja_parser.py @@ -16,40 +16,81 @@ # https://fuchsia.googlesource.com/infra/recipes/+/336933647862a1a9718b4ca18f0a67e89c2419f8/recipe_modules/ninja/resources/ninja_wrapper.py """Extracts a concise error from a ninja log.""" +import argparse +import logging from pathlib import Path import re +from typing import List, IO +import sys -_RULE_RE = re.compile(r'^\s*\[\d+/\d+\] (\S+)') -_FAILED_RE = re.compile(r'^\s*FAILED: (.*)$') -_FAILED_END_RE = re.compile(r'^\s*ninja: build stopped:.*') +_LOG: logging.Logger = logging.getLogger(__name__) +# Assume any of these lines could be prefixed with ANSI color codes. +_COLOR_CODES_PREFIX = r'^(?:\x1b)?(?:\[\d+m\s*)?' -def parse_ninja_stdout(ninja_stdout: Path) -> str: - """Extract an error summary from ninja output.""" +_GOOGLETEST_FAILED, _GOOGLETEST_RUN, _GOOGLETEST_OK, _GOOGLETEST_DISABLED = ( + '[ FAILED ]', + '[ RUN ]', + '[ OK ]', + '[ DISABLED ]', +) - failure_begins = False - failure_lines = [] - last_line = '' - with ninja_stdout.open() as ins: - for line in ins: - # Trailing whitespace isn't significant, as it doesn't affect the - # way the line shows up in the logs. However, leading whitespace may - # be significant, especially for compiler error messages. - line = line.rstrip() - if failure_begins: - if not _RULE_RE.match(line) and not _FAILED_END_RE.match(line): - failure_lines.append(line) - else: - # Output of failed step ends, save its info. - failure_begins = False +def _remove_passing_tests(failure_lines: List[str]) -> List[str]: + test_lines: List[str] = [] + result = [] + for line in failure_lines: + if test_lines: + if _GOOGLETEST_OK in line: + test_lines = [] + elif _GOOGLETEST_FAILED in line: + result.extend(test_lines) + test_lines = [] + result.append(line) else: - failed_nodes_match = _FAILED_RE.match(line) - failure_begins = False - if failed_nodes_match: - failure_begins = True - failure_lines.extend([last_line, line]) - last_line = line + test_lines.append(line) + elif _GOOGLETEST_RUN in line: + test_lines.append(line) + elif _GOOGLETEST_DISABLED in line: + pass + else: + result.append(line) + result.extend(test_lines) + return result + + +def _parse_ninja(ins: IO) -> str: + failure_lines: List[str] = [] + last_line: str = '' + + for line in ins: + _LOG.debug('processing %r', line) + # Trailing whitespace isn't significant, as it doesn't affect the + # way the line shows up in the logs. However, leading whitespace may + # be significant, especially for compiler error messages. + line = line.rstrip() + + if failure_lines: + _LOG.debug('inside failure block') + + if re.match(_COLOR_CODES_PREFIX + r'\[\d+/\d+\] (\S+)', line): + _LOG.debug('next rule started, ending failure block') + break + + if re.match(_COLOR_CODES_PREFIX + r'ninja: build stopped:.*', line): + _LOG.debug('ninja build stopped, ending failure block') + break + failure_lines.append(line) + + else: + if re.match(_COLOR_CODES_PREFIX + r'FAILED: (.*)$', line): + _LOG.debug('starting failure block') + failure_lines.extend([last_line, line]) + elif 'FAILED' in line: + _LOG.debug('not a match') + last_line = line + + failure_lines = _remove_passing_tests(failure_lines) # Remove "Requirement already satisfied:" lines since many of those might # be printed during Python installation, and they usually have no relevance @@ -60,5 +101,25 @@ def parse_ninja_stdout(ninja_stdout: Path) -> str: if not x.lstrip().startswith('Requirement already satisfied:') ] - result = '\n'.join(failure_lines) + result: str = '\n'.join(failure_lines) return re.sub(r'\n+', '\n', result) + + +def parse_ninja_stdout(ninja_stdout: Path) -> str: + """Extract an error summary from ninja output.""" + with ninja_stdout.open() as ins: + return _parse_ninja(ins) + + +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument('input', type=argparse.FileType('r')) + parser.add_argument('output', type=argparse.FileType('w')) + args = parser.parse_args(argv) + + for line in _parse_ninja(args.input): + args.output.write(line) + + +if __name__ == '__main__': + sys.exit(main()) |