Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2014 The Chromium Authors. All rights reserved. | 1 # Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 from collections import defaultdict | |
| 6 import copy | |
| 5 from datetime import datetime | 7 from datetime import datetime |
| 6 import os | 8 import os |
| 7 | 9 |
| 8 from google.appengine.api import users | 10 from google.appengine.api import users |
| 9 | 11 |
| 10 from base_handler import BaseHandler | 12 from base_handler import BaseHandler |
| 11 from base_handler import Permission | 13 from base_handler import Permission |
| 14 from handlers import handlers_util | |
| 15 from handlers import result_status | |
| 16 from handlers.result_status import NO_TRY_JOB_REASON_MAP | |
| 17 from model import wf_analysis_status | |
| 12 from model.wf_analysis import WfAnalysis | 18 from model.wf_analysis import WfAnalysis |
| 13 from model.wf_analysis_result_status import RESULT_STATUS_TO_DESCRIPTION | 19 from model.wf_analysis_result_status import RESULT_STATUS_TO_DESCRIPTION |
| 14 from waterfall import build_failure_analysis_pipelines | 20 from waterfall import build_failure_analysis_pipelines |
| 15 from waterfall import buildbot | 21 from waterfall import buildbot |
| 16 from waterfall import waterfall_config | 22 from waterfall import waterfall_config |
| 17 | 23 |
| 18 | 24 |
| 19 BUILD_FAILURE_ANALYSIS_TASKQUEUE = 'build-failure-analysis-queue' | 25 BUILD_FAILURE_ANALYSIS_TASKQUEUE = 'build-failure-analysis-queue' |
| 20 | 26 |
| 21 | 27 |
| (...skipping 17 matching lines...) Expand all Loading... | |
| 39 datetime.utcfromtimestamp(triage_record['triage_timestamp'])), | 45 datetime.utcfromtimestamp(triage_record['triage_timestamp'])), |
| 40 'user_name': triage_record['user_name'], | 46 'user_name': triage_record['user_name'], |
| 41 'result_status': RESULT_STATUS_TO_DESCRIPTION.get( | 47 'result_status': RESULT_STATUS_TO_DESCRIPTION.get( |
| 42 triage_record['result_status']), | 48 triage_record['result_status']), |
| 43 'version': triage_record.get('version'), | 49 'version': triage_record.get('version'), |
| 44 }) | 50 }) |
| 45 | 51 |
| 46 return triage_history | 52 return triage_history |
| 47 | 53 |
| 48 | 54 |
| 55 def _GetOrganizedAnalysisResultBySuspectedCL(analysis_result): | |
| 56 """Group tests it they have the same suspected CLs.""" | |
| 57 | |
|
stgao
2016/03/28 23:29:26
nit: empty line.
chanli
2016/03/29 01:35:56
Done.
| |
| 58 organized_results = defaultdict(list) | |
| 59 | |
| 60 if not analysis_result: | |
| 61 return organized_results | |
| 62 | |
| 63 for failure in analysis_result.get('failures', []): | |
|
stgao
2016/03/28 23:29:26
Maybe rename ``failure`` to ``step_failure`` to im
chanli
2016/03/29 01:35:56
Done.
| |
| 64 step_name = failure['step_name'] | |
| 65 supported = failure.get('supported', True) | |
| 66 step_revisions_index = {} | |
| 67 organized_suspected_cls = organized_results[step_name] | |
| 68 | |
| 69 if not failure.get('tests'): | |
| 70 # Non swraming, just group the whole step together. | |
|
lijeffrey
2016/03/29 00:29:38
nit: swarming
chanli
2016/03/29 01:35:56
Done.
| |
| 71 shared_result = { | |
| 72 'first_failure': failure['first_failure'], | |
| 73 'last_pass': failure.get('last_pass'), | |
| 74 'supported': supported, | |
| 75 'tests': [], | |
| 76 'suspected_cls': failure['suspected_cls'] | |
| 77 } | |
| 78 organized_suspected_cls.append(shared_result) | |
| 79 continue | |
| 80 | |
| 81 # Swarming tests. | |
| 82 for index, cl in enumerate(failure['suspected_cls']): | |
| 83 step_revisions_index[cl['revision']] = index | |
| 84 | |
| 85 # Groups tests by suspected CLs' revision. | |
|
stgao
2016/03/28 23:29:26
Maybe explain what does the key in the dict mean?
chanli
2016/03/29 01:35:57
Done.
| |
| 86 # Format is as below: | |
| 87 # { | |
| 88 # 1: { | |
| 89 # 'tests': ['test1', 'test2'], | |
| 90 # 'revisions': ['rev1'], | |
| 91 # 'suspected_cls': [ | |
| 92 # # suspected cl info for rev1 at step level. | |
| 93 # ] | |
| 94 # }, | |
| 95 # 3: { | |
| 96 # 'tests': ['test3'], | |
| 97 # 'revisions': ['rev3', 'rev2'], | |
| 98 # 'suspected_cls': [ | |
| 99 # # suspected cl info for rev2, rev3 at step level. | |
| 100 # ] | |
| 101 # } | |
| 102 # } | |
| 103 tests_group = defaultdict(list) | |
| 104 for index, test in enumerate(failure['tests']): | |
| 105 # Get all revisions for this test and check if there is | |
| 106 # any other test has the same revision set. | |
| 107 test_name = test['test_name'] | |
| 108 found_group = False | |
| 109 revisions = set() | |
| 110 for cl in test['suspected_cls']: | |
| 111 revisions.add(cl['revision']) | |
| 112 for group in tests_group.values(): | |
| 113 # Found tests that have the same revision, add current test to group. | |
|
stgao
2016/03/28 23:29:26
"same revision" -> "same culprits"? Possibly updat
chanli
2016/03/29 01:35:56
Done.
| |
| 114 if revisions == set(group['revisions']): | |
| 115 group['tests'].append(test_name) | |
| 116 found_group = True | |
| 117 break | |
| 118 if not found_group: | |
| 119 # First test with that revision set, add a new group. | |
| 120 group_suspected_cls = [] | |
| 121 for revision in revisions: | |
| 122 group_suspected_cls.append( | |
| 123 failure['suspected_cls'][step_revisions_index[revision]]) | |
| 124 tests_group[index] = { | |
| 125 'tests': [test_name], | |
| 126 'revisions': list(revisions), | |
| 127 'suspected_cls': group_suspected_cls | |
| 128 } | |
| 129 | |
| 130 for index, group in tests_group.iteritems(): | |
|
stgao
2016/03/28 23:29:26
How about a comment for what this logic block is f
chanli
2016/03/29 01:35:56
Done.
| |
| 131 test_result = failure['tests'][index] | |
| 132 shared_result = { | |
| 133 'first_failure': test_result['first_failure'], | |
| 134 'last_pass': test_result.get('last_pass'), | |
| 135 'supported': supported, | |
| 136 'tests': group['tests'], | |
| 137 'suspected_cls': group['suspected_cls'] | |
| 138 } | |
| 139 organized_suspected_cls.append(shared_result) | |
| 140 | |
| 141 return organized_results | |
| 142 | |
| 143 | |
| 144 def _GetAnalysisResultWithTryJobInfo( | |
| 145 organized_results, master_name, builder_name, build_number): | |
| 146 """Reorganizes analysis result and try job result by step_name and culprit. | |
| 147 | |
| 148 Returns: | |
| 149 update_result (dict): A dict of classified results. | |
| 150 | |
| 151 The format for those dicts are as below: | |
| 152 { | |
| 153 # A dict of results that contains both | |
| 154 # heuristic analysis results and try job results. | |
| 155 'determined_results': { | |
| 156 'step1': { | |
| 157 'results': [ | |
| 158 { | |
| 159 'try_job':{ | |
| 160 'ref_name': 'step1', | |
| 161 'try_job_key': 'm/b/119', | |
| 162 'status': wf_analysis_status.ANALYZED, | |
| 163 'try_job_url': 'url/121', | |
| 164 'try_job_build_number': 121, | |
| 165 }, | |
| 166 'heuristic_analysis': { | |
| 167 'suspected_cls': [ | |
| 168 { | |
| 169 'build_number': 98, | |
| 170 'repo_name': 'chromium', | |
| 171 'revision': 'r98_1', | |
| 172 'commit_position': None, | |
| 173 'url': None, | |
| 174 'score': 5, | |
| 175 'hints': { | |
| 176 'added f98.cc (and it was in log)': 5, | |
| 177 }, | |
| 178 } | |
| 179 ] | |
| 180 } | |
| 181 'tests': ['test1', 'test2'], | |
| 182 'first_failure': 98, | |
| 183 'last_pass': 97, | |
| 184 'supported': True | |
| 185 } | |
| 186 ] | |
| 187 } | |
| 188 }, | |
| 189 # A dict of result for flaky tests. | |
| 190 'flaky_results': {...}, | |
| 191 # A dict of results for all the other conditions, | |
| 192 # such as non-swarming tests or swarming rerun failed. | |
| 193 'undetermined_results': {...} | |
| 194 } | |
| 195 """ | |
| 196 update_results = defaultdict(lambda: defaultdict(lambda: defaultdict(list))) | |
| 197 | |
| 198 if not organized_results: | |
| 199 return update_results | |
| 200 | |
| 201 try_job_info = handlers_util.GetAllTryJobResults( | |
| 202 master_name, builder_name, build_number) | |
| 203 if not try_job_info: | |
| 204 return update_results | |
| 205 | |
| 206 for step_name, try_jobs in try_job_info.iteritems(): | |
| 207 try_jobs = try_jobs['try_jobs'] | |
| 208 step_heuristic_results = organized_results[step_name] | |
| 209 step_updated_results = update_results[step_name]['results'] | |
|
stgao
2016/03/28 23:29:26
To be consistent, should we rename ``update_result
lijeffrey
2016/03/29 00:29:38
+1
chanli
2016/03/29 01:35:56
Done.
| |
| 210 | |
| 211 # Finds out try job result index and heuristic result index for each test. | |
| 212 test_result_map = defaultdict(lambda: defaultdict(int)) | |
| 213 | |
| 214 for index, try_job in enumerate(try_jobs): | |
| 215 if not try_job.get('tests'): # Compile or non-swarming. | |
| 216 test_result_map['non-swarming']['try_job_index'] = index | |
| 217 continue | |
| 218 for test_name in try_job['tests']: | |
| 219 test_result_map[test_name]['try_job_index'] = index | |
| 220 for index, heuristic_result in enumerate(step_heuristic_results): | |
|
lijeffrey
2016/03/29 00:29:38
nit: 1 blank line before this for
chanli
2016/03/29 01:35:57
Done.
| |
| 221 if not heuristic_result.get('tests'): # Compile or non-swarming. | |
| 222 test_result_map['non-swarming']['heuristic_index'] = index | |
| 223 continue | |
| 224 for test_name in heuristic_result['tests']: | |
| 225 test_result_map[test_name]['heuristic_index'] = index | |
| 226 | |
| 227 # Group tests based on indices. | |
| 228 indices_test_map = defaultdict(list) | |
| 229 for test_name, indices in test_result_map.iteritems(): | |
| 230 indices_test_map[ | |
| 231 (indices['try_job_index'], indices['heuristic_index'])].append( | |
|
stgao
2016/03/28 23:29:26
What if the test has result only from heuristic or
chanli
2016/03/29 01:35:57
There should still be entries for those tests, jus
| |
| 232 test_name) | |
| 233 | |
| 234 for indices, tests in indices_test_map.iteritems(): | |
|
stgao
2016/03/28 23:29:26
For readability, how about below if it works?
for
chanli
2016/03/29 01:35:56
Done.
| |
| 235 try_job_result = try_jobs[indices[0]] | |
| 236 heuristic_result = step_heuristic_results[indices[1]] | |
| 237 | |
| 238 final_result = { | |
| 239 'try_job': try_job_result, | |
| 240 'heuristic_analysis': { | |
| 241 'suspected_cls': heuristic_result['suspected_cls'] | |
| 242 }, | |
| 243 'tests': tests if tests != ['non-swarming'] else [], | |
|
stgao
2016/03/28 23:29:26
Maybe make 'non-swarming' a singleton python objec
chanli
2016/03/29 01:35:56
Done.
| |
| 244 'first_failure': heuristic_result['first_failure'], | |
| 245 'last_pass': heuristic_result['last_pass'], | |
| 246 'supported': heuristic_result['supported'] | |
| 247 } | |
| 248 | |
| 249 if try_job_result['status'] == result_status.FLAKY: | |
| 250 step_updated_results['flaky_results'].append(final_result) | |
| 251 elif try_job_result['status'] in NO_TRY_JOB_REASON_MAP.values(): | |
| 252 # There is no try job info but only heuristic result. | |
| 253 step_updated_results['undetermined_results'].append(final_result) | |
| 254 else: | |
| 255 step_updated_results['determined_results'].append(final_result) | |
| 256 | |
| 257 return update_results | |
| 258 | |
| 259 | |
| 49 class BuildFailure(BaseHandler): | 260 class BuildFailure(BaseHandler): |
| 50 PERMISSION_LEVEL = Permission.ANYONE | 261 PERMISSION_LEVEL = Permission.ANYONE |
| 51 | 262 |
| 52 def _ShowDebugInfo(self): | 263 def _ShowDebugInfo(self): |
| 53 # Show debug info only if the app is run locally during development, if the | 264 # Show debug info only if the app is run locally during development, if the |
| 54 # currently logged-in user is an admin, or if it is explicitly requested | 265 # currently logged-in user is an admin, or if it is explicitly requested |
| 55 # with parameter 'debug=1'. | 266 # with parameter 'debug=1'. |
| 56 return (os.environ['SERVER_SOFTWARE'].startswith('Development') or | 267 return (os.environ['SERVER_SOFTWARE'].startswith('Development') or |
| 57 users.is_current_user_admin() or self.request.get('debug') == '1') | 268 users.is_current_user_admin() or self.request.get('debug') == '1') |
| 58 | 269 |
| (...skipping 29 matching lines...) Expand all Loading... | |
| 88 # Only allow admin to force a re-run and set the build_completed. | 299 # Only allow admin to force a re-run and set the build_completed. |
| 89 force = (users.is_current_user_admin() and | 300 force = (users.is_current_user_admin() and |
| 90 self.request.get('force') == '1') | 301 self.request.get('force') == '1') |
| 91 build_completed = (users.is_current_user_admin() and | 302 build_completed = (users.is_current_user_admin() and |
| 92 self.request.get('build_completed') == '1') | 303 self.request.get('build_completed') == '1') |
| 93 analysis = build_failure_analysis_pipelines.ScheduleAnalysisIfNeeded( | 304 analysis = build_failure_analysis_pipelines.ScheduleAnalysisIfNeeded( |
| 94 master_name, builder_name, build_number, | 305 master_name, builder_name, build_number, |
| 95 build_completed=build_completed, force=force, | 306 build_completed=build_completed, force=force, |
| 96 queue_name=BUILD_FAILURE_ANALYSIS_TASKQUEUE) | 307 queue_name=BUILD_FAILURE_ANALYSIS_TASKQUEUE) |
| 97 | 308 |
| 309 organized_results = _GetOrganizedAnalysisResultBySuspectedCL( | |
| 310 analysis.result) | |
| 311 analysis_result = _GetAnalysisResultWithTryJobInfo( | |
| 312 organized_results, *build_info) | |
| 313 | |
| 98 data = { | 314 data = { |
| 99 'master_name': analysis.master_name, | 315 'master_name': analysis.master_name, |
| 100 'builder_name': analysis.builder_name, | 316 'builder_name': analysis.builder_name, |
| 101 'build_number': analysis.build_number, | 317 'build_number': analysis.build_number, |
| 102 'pipeline_status_path': analysis.pipeline_status_path, | 318 'pipeline_status_path': analysis.pipeline_status_path, |
| 103 'show_debug_info': self._ShowDebugInfo(), | 319 'show_debug_info': self._ShowDebugInfo(), |
| 104 'analysis_request_time': _FormatDatetime(analysis.request_time), | 320 'analysis_request_time': _FormatDatetime(analysis.request_time), |
| 105 'analysis_start_time': _FormatDatetime(analysis.start_time), | 321 'analysis_start_time': _FormatDatetime(analysis.start_time), |
| 106 'analysis_end_time': _FormatDatetime(analysis.end_time), | 322 'analysis_end_time': _FormatDatetime(analysis.end_time), |
| 107 'analysis_duration': analysis.duration, | 323 'analysis_duration': analysis.duration, |
| 108 'analysis_update_time': _FormatDatetime(analysis.updated_time), | 324 'analysis_update_time': _FormatDatetime(analysis.updated_time), |
| 109 'analysis_completed': analysis.completed, | 325 'analysis_completed': analysis.completed, |
| 110 'analysis_failed': analysis.failed, | 326 'analysis_failed': analysis.failed, |
| 111 'analysis_result': analysis.result, | 327 'analysis_result': analysis_result, |
| 112 'analysis_correct': analysis.correct, | 328 'analysis_correct': analysis.correct, |
| 113 'triage_history': _GetTriageHistory(analysis), | 329 'triage_history': _GetTriageHistory(analysis), |
| 114 'show_triage_help_button': self._ShowTriageHelpButton(), | 330 'show_triage_help_button': self._ShowTriageHelpButton(), |
| 331 'status_message_map': result_status.ToStatusMessage() | |
| 115 } | 332 } |
| 116 | 333 |
| 117 return {'template': 'build_failure.html', 'data': data} | 334 return {'template': 'build_failure.html', 'data': data} |
| 118 | 335 |
| 119 def HandlePost(self): # pragma: no cover | 336 def HandlePost(self): # pragma: no cover |
| 120 return self.HandleGet() | 337 return self.HandleGet() |
| OLD | NEW |