1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
|
# 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) <info>
# 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)
|