Chromium Code Reviews| 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. | |
|
qyearsley
2015/01/16 22:55:26
The first line of the docstring should use third-p
stgao
2015/01/21 20:29:22
Done for files in this CL.
Will do a cleanup of ot
| |
| 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 _NormalizeObjectFile(file_path): | |
| 27 # During compile, a/b/c/file.cc in TARGET will be compiled into object | |
| 28 # file a/b/c/TARGET.file.o, thus TARGET needs removing from path. | |
| 29 if file_path.startswith('obj/'): | |
| 30 file_path = file_path[4:] | |
| 31 | |
| 32 file_dir = os.path.dirname(file_path) | |
| 33 | |
|
qyearsley
2015/01/16 22:55:25
Probably no need to have visual separation above a
stgao
2015/01/21 20:29:23
Done.
| |
| 34 file_name = os.path.basename(file_path) | |
| 35 parts = file_name.split('.', 1) | |
| 36 if len(parts) == 2 and parts[1].endswith('.o'): | |
| 37 file_name = parts[1] | |
| 38 | |
| 39 return os.path.join(file_dir, file_name).replace(os.sep, '/') | |
| 40 | |
| 41 | |
| 42 _COMMON_SUFFIXES= [ | |
|
qyearsley
2015/01/16 22:55:25
Missing a space after "=".
stgao
2015/01/21 20:29:22
Ooops, done.
| |
| 43 'impl', | |
| 44 'gcc', 'msvc', | |
| 45 'arm', 'arm64', 'mips', 'portable', 'x86', | |
| 46 'android', 'ios', 'linux', 'mac', 'ozone', 'posix', 'win', | |
| 47 'aura', 'x', 'x11', | |
| 48 ] | |
| 49 | |
| 50 _COMMON_SUFFIX_PATTERNS = [ | |
| 51 re.compile('.*(\_[^\_]*test)$'), | |
|
qyearsley
2015/01/16 22:55:25
This also matches "foo_blahtest", which means the
stgao
2015/01/21 20:29:22
OK. Restrict this pattern to a list of common suff
| |
| 52 ] + [ | |
| 53 re.compile('.*(\_%s)$' % postfix) for postfix in _COMMON_SUFFIXES | |
| 54 ] | |
| 55 | |
| 56 | |
| 57 def _StripExtensionAndCommonPostfix(file_path): | |
| 58 """Strip extension and common postfixes from file name to guess correlation. | |
|
qyearsley
2015/01/16 22:55:26
Strip -> Strips
Postfix -> Suffix
stgao
2015/01/21 20:29:22
Done.
| |
| 59 | |
| 60 Examples: | |
| 61 file_impl.cc, file_unittest.cc, file_impl_mac.h -> file | |
| 62 """ | |
| 63 file_dir = os.path.dirname(file_path) | |
| 64 file_name = os.path.splitext(os.path.basename(file_path))[0] | |
| 65 while True: | |
| 66 match = None | |
| 67 for postfix_patten in _COMMON_SUFFIX_PATTERNS: | |
|
qyearsley
2015/01/16 22:55:25
postfix_patten -> suffix_pattern
stgao
2015/01/21 20:29:23
Done.
| |
| 68 match = postfix_patten.match(file_name) | |
| 69 if match: | |
| 70 file_name = file_name[:-len(match.group(1))] | |
| 71 break | |
| 72 | |
| 73 if not match: | |
| 74 break | |
| 75 | |
| 76 return os.path.join(file_dir, file_name).replace(os.sep, '/') | |
| 77 | |
| 78 | |
| 79 def _IsCorrelated(src_file, file_path): | |
| 80 """Check if two files are correlated. | |
|
qyearsley
2015/01/16 22:55:25
Maybe just "related", rather than "correlated"?
stgao
2015/01/21 20:29:22
Done.
| |
| 81 | |
| 82 Example of correlated files: | |
| 83 1. file.h <-> file_impl.cc | |
| 84 2. file_impl.cc <-> file_unittest.cc | |
| 85 3. file_win.cc <-> file_mac.cc | |
| 86 4. a/b/x.cc <-> a/b/y.cc | |
| 87 5. x.h <-> x.cc | |
| 88 """ | |
| 89 if file_path.endswith('.o'): | |
| 90 file_path = _NormalizeObjectFile(file_path) | |
| 91 | |
| 92 if _IsSameFile(_StripExtensionAndCommonPostfix(src_file), | |
| 93 _StripExtensionAndCommonPostfix(file_path)): | |
| 94 return True | |
| 95 | |
| 96 # Two file are in the same directory: a/b/x.cc <-> a/b/y.cc | |
| 97 # TODO: cause noisy result? | |
| 98 src_file_dir = os.path.dirname(src_file) | |
| 99 return src_file_dir and src_file_dir == os.path.dirname(file_path) | |
| 100 | |
| 101 | |
| 102 class _Justification(object): | |
|
qyearsley
2015/01/16 22:55:25
What does this class contain/represent? Evidence f
stgao
2015/01/21 20:29:23
Added a docstring to explain its usage, hints, and
| |
| 103 def __init__(self): | |
| 104 self.suspects = 0 | |
| 105 self.scores = 0 | |
|
qyearsley
2015/01/16 22:55:26
Using a plural word in a variable name usually sug
stgao
2015/01/21 20:29:22
Done.
Explanation of the difference between the s
| |
| 106 self.hints = list() | |
|
qyearsley
2015/01/16 22:55:26
Do these all need to be public? Also, you could us
stgao
2015/01/21 20:29:23
Done.
| |
| 107 | |
| 108 def AddFileChange(self, change_action, src_file, file_path, suspect, score): | |
| 109 """Add a suspected file change. | |
| 110 | |
| 111 Args: | |
| 112 change_action (str): The change type (modify/add/delete) of the file. | |
|
qyearsley
2015/01/16 22:55:26
Saying what the arg type is is useful, but args se
stgao
2015/01/21 20:29:22
I also get used to the docstring format as you sug
qyearsley
2015/01/27 00:58:48
Alright, that looks good -- if it's in the style g
| |
| 113 src_file (str): The file changed by a CL. | |
| 114 file_path (str): The file showing in the failure log. | |
| 115 suspect (int): Suspect points for the file change. | |
| 116 score (int): Score for the file change. | |
| 117 """ | |
| 118 self.suspects += suspect | |
| 119 self.scores += score | |
| 120 | |
| 121 # TODO: make hint more descriptive? | |
| 122 if src_file != file_path: | |
| 123 self.hints.append( | |
| 124 '%s %s (%s)' % (change_action, src_file, file_path)) | |
| 125 else: | |
| 126 self.hints.append('%s %s' % (change_action, src_file)) | |
| 127 | |
| 128 def ToDict(self): | |
| 129 return { | |
| 130 'suspects': self.suspects, | |
| 131 'scores': self.scores, | |
| 132 'hints': self.hints, | |
| 133 } | |
| 134 | |
| 135 | |
| 136 def _CheckFile( | |
| 137 change_action, src_file, file_path, suspect, score, justification): | |
| 138 """Check if the given files are the same or correlated. | |
| 139 | |
| 140 Args: | |
| 141 change_action (str): The change type (modify/add/delete) of the file. | |
| 142 src_file (str): The file changed by a CL. | |
| 143 file_path (str): The file showing in the failure log. | |
| 144 suspect (int): Suspect points if the two files are the same. | |
| 145 score (int): Score if the two files are the same. | |
| 146 justification (_Justification): An instance of _Justification. | |
| 147 """ | |
| 148 if _IsSameFile(src_file, file_path): | |
| 149 justification.AddFileChange( | |
| 150 change_action, src_file, file_path, suspect, score) | |
| 151 elif _IsCorrelated(src_file, file_path): | |
| 152 # For correlated files, do suspect=0 and score=1, because it is not highly | |
| 153 # suspected. | |
| 154 justification.AddFileChange( | |
| 155 change_action, src_file, file_path, 0, 1) | |
| 156 | |
| 157 | |
| 158 def CheckFiles(failure_signal, change_log): | |
|
qyearsley
2015/01/16 22:55:26
Non-trivial public functions (CheckFiles and Analy
stgao
2015/01/21 20:29:22
Done.
| |
| 159 justification = _Justification() | |
| 160 | |
| 161 for file_path, _ in failure_signal.files.iteritems(): | |
| 162 # TODO(stgao): remove this hack when DEPS parsing is supported. | |
| 163 if file_path.startswith('src/'): | |
| 164 file_path = file_path[4:] | |
| 165 | |
| 166 for touched_file in change_log['touched_files']: | |
| 167 change_type = touched_file['change_type'] | |
| 168 | |
| 169 if change_type == ChangeType.MODIFY: | |
| 170 if _IsSameFile(touched_file['new_path'], file_path): | |
| 171 # TODO: use line number for git blame. | |
| 172 justification.AddFileChange( | |
| 173 'modify', touched_file['new_path'], file_path, 0, 1) | |
| 174 elif _IsCorrelated(touched_file['new_path'], file_path): | |
| 175 justification.AddFileChange( | |
| 176 'modify', touched_file['new_path'], file_path, 0, 1) | |
| 177 | |
| 178 if change_type in (ChangeType.ADD, ChangeType.COPY, ChangeType.RENAME): | |
| 179 _CheckFile('add', touched_file['new_path'], file_path, 1, 5, | |
| 180 justification) | |
| 181 | |
| 182 if change_type in (ChangeType.DELETE, ChangeType.RENAME): | |
| 183 _CheckFile('delete', touched_file['old_path'], file_path, 1, 5, | |
| 184 justification) | |
| 185 | |
| 186 if not justification.scores: | |
| 187 return None | |
| 188 else: | |
| 189 return justification.ToDict() | |
| 190 | |
| 191 | |
| 192 def AnalyzeBuildFailure(failure_info, change_logs, failure_signals): | |
| 193 analysis_result = {} | |
| 194 | |
| 195 if not failure_info['failed']: | |
| 196 return analysis_result | |
| 197 | |
| 198 failed_steps = failure_info['failed_steps'] | |
| 199 builds = failure_info['builds'] | |
| 200 for step_name, step_failure_info in failed_steps.iteritems(): | |
| 201 failure_signal = FailureSignal.FromJson(failure_signals[step_name]) | |
| 202 failed_build_number = step_failure_info['current_failure'] | |
| 203 build_number = step_failure_info['first_failure'] | |
| 204 | |
| 205 step_analysis_result = {} | |
| 206 | |
| 207 while build_number <= failed_build_number: | |
| 208 for revision in builds[str(build_number)]['blame_list']: | |
| 209 justification = CheckFiles(failure_signal, change_logs[revision]) | |
|
qyearsley
2015/01/16 22:55:26
Maybe justification_dict, since this (as is) is ei
stgao
2015/01/21 20:29:22
Done.
| |
| 210 if justification: | |
| 211 step_analysis_result[revision] = justification | |
| 212 | |
| 213 build_number += 1 | |
| 214 | |
| 215 if step_analysis_result: | |
| 216 # TODO: sorted CLs related to a step failure. | |
| 217 analysis_result[step_name] = step_analysis_result | |
| 218 | |
| 219 return analysis_result | |
| OLD | NEW |