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. | |
| 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 |