OLD | NEW |
---|---|
(Empty) | |
1 # Copyright 2015 The Chromium Authors. All rights reserved. | |
2 # Use of this source code is governed by a BSD-style license that can be | |
3 # found in the LICENSE file. | |
4 | |
5 import collections | |
6 import os | |
7 import re | |
8 | |
9 from common.diff import ChangeType | |
10 from waterfall.failure_signal import FailureSignal | |
11 | |
12 | |
13 def IsSameFile(src_file, file_path): | |
14 """Guess if the two files are the same. | |
15 | |
16 File paths from a failure might not be full paths. So match string by postfix. | |
17 Examples: | |
18 src/chrome/test/base/chrome_process_util.h <-> base/chrome_process_util.h | |
19 """ | |
20 if os.path.basename(src_file) != os.path.basename(file_path): | |
21 return False | |
22 else: | |
23 return src_file.endswith(file_path) | |
24 | |
25 | |
26 def JoinAsFilePath(file_dir, file_name): | |
qyearsley
2015/01/15 19:10:28
Should this function be marked as private? (Same q
stgao
2015/01/16 20:21:38
Done.
| |
27 if file_dir: | |
28 return '%s/%s' % (file_dir, file_name) | |
29 else: | |
30 return file_name | |
31 | |
32 | |
33 def NormalizeObjectFile(file_path): | |
34 # During compile, a/b/c/file.cc in TARGET will be compiled into object | |
35 # file a/b/c/TARGET.file.o, thus TARGET needs removing from path. | |
36 if file_path.startswith('obj/'): | |
37 file_path = file_path[4:] | |
38 | |
39 file_dir = os.path.dirname(file_path) | |
40 | |
41 file_name = os.path.basename(file_path) | |
42 parts = file_name.split('.', 1) | |
43 if len(parts) == 2 and parts[1].endswith('.o'): | |
44 file_name = parts[1] | |
45 | |
46 return JoinAsFilePath(file_dir, file_name) | |
qyearsley
2015/01/15 21:15:03
For both uses of JoinAsFilePath in this file, you
stgao
2015/01/16 20:21:38
Done.
Switch to os.path.join, but need to do a re
qyearsley
2015/01/16 22:55:25
Ah, very observant. Sounds good.
| |
47 | |
48 | |
49 COMMON_POSTFIXES = [ | |
qyearsley
2015/01/15 19:10:28
1) s/POSTFIX/SUFFIX/
2) If this is only used in th
stgao
2015/01/16 20:21:39
Done.
| |
50 'impl', | |
51 'gcc', 'msvc', | |
52 'arm', 'arm64', 'mips', 'portable', 'x86', | |
53 'android', 'ios', 'linux', 'mac', 'ozone', 'posix', 'win', | |
54 'aura', 'x', 'x11'] | |
qyearsley
2015/01/15 19:10:28
Style nit: Closing parenthesis goes on its own lin
stgao
2015/01/16 20:21:38
Done.
| |
55 | |
56 COMMON_POSTFIX_PATTERNS = [ | |
57 re.compile('.*(\_[^\_]*test)$'), | |
58 ] + [ | |
59 re.compile('.*(\_%s)$' % postfix) for postfix in COMMON_POSTFIXES | |
60 ] | |
qyearsley
2015/01/15 19:10:28
It seems like these four lines should be un-indent
stgao
2015/01/16 20:21:38
Done.
| |
61 | |
62 | |
63 def StripExtensionAndCommonPostfix(file_path): | |
64 """Strip extension and common postfixes from file name to guess correlation. | |
65 | |
66 Examples: | |
67 file_impl.cc, file_unittest.cc, file_impl_mac.h -> file | |
68 """ | |
69 file_dir = os.path.dirname(file_path) | |
70 file_name = os.path.splitext(os.path.basename(file_path))[0] | |
71 while True: | |
72 match = None | |
73 for postfix_patten in COMMON_POSTFIX_PATTERNS: | |
74 match = postfix_patten.match(file_name) | |
75 if match: | |
76 file_name = file_name[:-len(match.group(1))] | |
77 break | |
78 | |
79 if not match: | |
80 break | |
81 | |
82 return JoinAsFilePath(file_dir, file_name) | |
83 | |
84 | |
85 def IsCorrelated(src_file, file_path): | |
86 """Check if two files are correlated. | |
87 | |
88 Examples: | |
89 1. file.h <-> file_impl.cc | |
90 2. file_impl.cc <-> file_unittest.cc | |
91 3. file_win.cc <-> file_mac.cc | |
92 4. a/b/x.cc <-> a/b/y.cc | |
93 5. x.h <-> x.cc | |
94 """ | |
95 if file_path.endswith('.o'): | |
96 file_path = NormalizeObjectFile(file_path) | |
97 | |
98 # Example: a/b/file_impl.cc <-> a/b/file_unittest.cc | |
99 if IsSameFile(StripExtensionAndCommonPostfix(src_file), | |
100 StripExtensionAndCommonPostfix(file_path)): | |
101 return True | |
102 | |
103 # Two file are in the same directory: a/b/x.cc <-> a/b/y.cc | |
104 # TODO: cause noisy result? | |
105 src_file_dir = os.path.dirname(src_file) | |
106 return src_file_dir and src_file_dir == os.path.dirname(file_path) | |
107 | |
108 | |
109 def CheckFiles(failure_signal, change_log): | |
110 result = { | |
111 'suspects' : 0, | |
112 'scores' : 0, | |
113 'hints' : [] | |
114 } | |
115 | |
116 def _AddHint(change_action, src_file, file_path): | |
qyearsley
2015/01/15 19:10:28
Nested functions don't actually need to be marked
stgao
2015/01/16 20:21:38
Done.
Moved _AddHint to a new class _Justificatio
| |
117 # TODO: make hint more descriptive? | |
118 if src_file != file_path: | |
119 result['hints'].append( | |
120 '%s %s (%s)' % (change_action, src_file, file_path)) | |
121 else: | |
122 result['hints'].append('%s %s' % (change_action, src_file)) | |
123 | |
124 def _CheckFile(change_action, src_file, file_path, suspect, score): | |
125 # 'suspects' and 'scores' already defined - pylint: disable=E0602, W0612 | |
126 if IsSameFile(src_file, file_path): | |
127 result['suspects'] += suspect | |
128 result['scores'] += score | |
129 _AddHint(change_action, src_file, file_path) | |
130 elif IsCorrelated(src_file, file_path): | |
131 result['scores'] += 1 | |
132 _AddHint(change_action, src_file, file_path) | |
133 | |
134 for file_path, _ in failure_signal.files.iteritems(): | |
135 # TODO(stgao): remove this hack when DEPS parsing is supported. | |
136 if file_path.startswith('src/'): | |
137 file_path = file_path[4:] | |
138 | |
139 for touched_file in change_log['touched_files']: | |
140 change_type = touched_file['change_type'] | |
141 | |
142 if change_type in (ChangeType.ADD, ChangeType.COPY, ChangeType.RENAME): | |
143 _CheckFile('add', touched_file['new_path'], file_path, 1, 5) | |
144 | |
145 if change_type == ChangeType.MODIFY: | |
146 if IsSameFile(touched_file['new_path'], file_path): | |
147 # TODO: use line number for git blame. | |
148 result['scores'] += 1 | |
149 _AddHint('modify', touched_file['new_path'], file_path) | |
150 continue | |
151 elif IsCorrelated(touched_file['new_path'], file_path): | |
152 result['scores'] += 1 | |
153 _AddHint('modify', touched_file['new_path'], file_path) | |
154 continue | |
155 | |
156 if change_type in (ChangeType.DELETE, ChangeType.RENAME): | |
157 _CheckFile('delete', touched_file['old_path'], file_path, 1, 5) | |
158 | |
159 if not result['scores']: | |
160 return None | |
161 else: | |
162 return result | |
163 | |
164 | |
165 def AnalyzeBuildFailure(failure_info, change_logs, failure_signals): | |
166 analysis_result = {} | |
167 | |
168 if not failure_info['failed']: | |
169 return analysis_result | |
170 | |
171 failed_steps = failure_info['failed_steps'] | |
172 builds = failure_info['builds'] | |
173 for step_name, step_failure_info in failed_steps.iteritems(): | |
174 failure_signal = FailureSignal.FromJson(failure_signals[step_name]) | |
175 failed_build_number = step_failure_info['current_failure'] | |
176 build_number = step_failure_info['first_failure'] | |
177 | |
178 step_analysis_result = {} | |
179 | |
180 while build_number <= failed_build_number: | |
181 for revision in builds[str(build_number)]['blame_list']: | |
182 justification = CheckFiles(failure_signal, change_logs[revision]) | |
183 if justification: | |
184 step_analysis_result[revision] = justification | |
185 | |
186 build_number += 1 | |
187 | |
188 if step_analysis_result: | |
189 # TODO: sorted CLs related to a step failure. | |
190 analysis_result[step_name] = step_analysis_result | |
191 | |
192 return analysis_result | |
OLD | NEW |