# Copyright (C) 2019 The Qt Company Ltd. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 import re import subprocess import sys import logging import gzip from typing import Set, List usage = """ Usage: parse_build_log.py [log_file] Parses the output of COIN test runs and prints short summaries of compile errors and test fails for usage as gerrit comment. Takes the file name (either text or compressed .gz file). """ # Match the log prefix "agent:2019/06/04 12:32:54 agent.go:262:" # and alternatively prefix with column(?): "agent:2019/06/04 12:32:54 agent.go:262: 53: " prefix_re = re.compile(r'^agent:[\d :/]+\w+\.go:\d+: (\d+: )?') # Match QTestlib output start_test_re = re.compile(r'^\*{9} Start testing of \w+ \*{9}$') end_test_re = re.compile(r'Totals: \d+ passed, (\d+) failed, \d+ skipped, \d+ blacklisted, \d+ms') end_test_crash_re = re.compile(r'\d+/\d+\sTest\s#\d+:.*\*\*\*Failed.*') end_test_timeout_re = re.compile(r'Test #\d+: .+ \.*\*{3}Timeout \d+\.\d+ sec') # Match make or cmake errors make_error_re = re.compile(r'make\[.*Error \d+$') cmake_error_re = re.compile(r'CMake Error') # Failed to install tests archive nosource_re = re.compile(r"No sources for \"https?://(\d+\.\d+\.\d+\.\d+):(\d+)") # failed to install tests archive def read_file(file_name): """ Read a text file into a list of of chopped lines. """ opener = gzip.open if file_name.endswith(".gz") else open with opener(file_name, mode="rt") as f: return [prefix_re.sub('', l.rstrip()) for l in f.readlines()] def is_compile_error(line): """ Return whether a line is an error from one of the common compilers (g++, MSVC, Python) """ if any(e in line for e in (": error: ", ": error C", "SyntaxError:", "NameError:", "fatal error")): # Ignore error messages in debug output # and also ignore the final ERROR building message, as that one would only print sccache # output if not ("QDEBUG" in line or "QWARN" in line): logging.debug(f"===> Found error in line \n{line}\n") return True has_error = make_error_re.match(line) or cmake_error_re.search(line) if has_error: logging.debug(f"===> Found error in line \n{line}\n") return has_error def print_failed_test(lines, start, end, already_known_errors): """ For a failed test, print 3 lines following the FAIL!/XPASS and header/footer. """ last_fail = -50 # Print 3 lines after a failure header = '\n{}: {}'.format(start, lines[start]) test_result = [] for i in range(start + 1, end): line = lines[i] if 'FAIL!' in line or 'XPASS' in line or '***Failed' in line: # the format is FAIL! class::method(n) # with n being the time that the test has been repeated. To deduplicate, we need to # remove n. n should be < 9, so %d is sufficient to match # The first test might not have a number at all, but then the regex will also just # ignore it adjusted_line = re.sub(r'\(\d\)', '', line) if not adjusted_line in already_known_errors: already_known_errors.add(adjusted_line) last_fail = i if i - last_fail < 4: test_result.append(line) if test_result: print(header) print("\n".join(test_result)) print('{}\n'.format(lines[end])) def is_fatal_timeout(line: str) -> bool: if "Killed process: No output received (timeout" in line: return True if end_test_timeout_re.match(line): return True def print_line_with_context(start_line_number: int, context_before: int, lines: List[str]): start = max(0, start_line_number - context_before) sys.stdout.write('\n{}: '.format(start)) for e in range(start, start_line_number + 1): print(lines[e]) def parse(lines): """ Parse the output and print compile/test errors. """ test_start_line = -1 within_configure_tests = False already_known_errors: Set[str] = set() # used to skip CMake output which contains information about failed configure tests within_cmake_output: bool = False # used to skip sccache output for i, line in enumerate(lines): if within_cmake_output: if "======== End CMake output ======" in line: within_cmake_output = False elif within_configure_tests: if line == 'Done running configuration tests.': within_configure_tests = False elif test_start_line >= 0: end_match = end_test_re.match(line) if end_match: fails = int(end_match.group(1)) if fails: print_failed_test(lines, test_start_line, i, already_known_errors) test_start_line = -1 elif end_test_crash_re.match(line): logging.debug(f"===> test crashed {line} {test_start_line} {i}") print_failed_test(lines, test_start_line, i, already_known_errors) test_start_line = -1 elif is_fatal_timeout(line): logging.debug("===> Matched fatal timeout") print("While running tests, a timeout occurred: ") print_line_with_context(i, 10, lines) # Do not report errors within configuration tests elif line == 'Running configuration tests...': within_configure_tests = True elif "======== CMake output ======" in line: within_cmake_output = True elif start_test_re.match(line): logging.debug("===> Matched test") test_start_line = i elif is_compile_error(line): logging.debug("===> Matched compile error") print_line_with_context(i, 10, lines) elif nosource_re.match(line): print_line_with_context(i, 10, lines) if __name__ == '__main__': if sys.version_info[0] != 3: print("This script requires Python 3") sys.exit(-2) if len(sys.argv) < 2: print(usage) sys.exit(-1) file_name = sys.argv[1] try: should_log = sys.argv[2] == "-debug" if should_log: logging.basicConfig(level=logging.DEBUG) except IndexError: pass lines = read_file(file_name) parse(lines)