blob: 487af17548dc7982967a963c048301890030ac51 [file] [log] [blame]
# Copyright 2022 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
# Inspired by
# 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
_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*)?'
_GOOGLETEST_FAILED, _GOOGLETEST_RUN, _GOOGLETEST_OK, _GOOGLETEST_DISABLED = (
'[ FAILED ]',
'[ RUN ]',
'[ OK ]',
'[ DISABLED ]',
)
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:
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
# to the actual error.
failure_lines = [
x
for x in failure_lines
if not x.lstrip().startswith('Requirement already satisfied:')
]
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())