| OLD | NEW |
| 1 # Copyright 2015 The Chromium Authors. All rights reserved. | 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 | 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 | 5 from collections import defaultdict |
| 6 import logging | 6 import logging |
| 7 | 7 |
| 8 from google.appengine.ext import ndb | 8 from google.appengine.ext import ndb |
| 9 | 9 |
| 10 from common.git_repository import GitRepository | 10 from common.git_repository import GitRepository |
| 11 from common.http_client_appengine import HttpClientAppengine as HttpClient | 11 from common.http_client_appengine import HttpClientAppengine as HttpClient |
| 12 from common.pipeline_wrapper import BasePipeline | 12 from common.pipeline_wrapper import BasePipeline |
| 13 from common.waterfall import failure_type | 13 from common.waterfall import failure_type |
| 14 from model import analysis_approach_type |
| 14 from model import analysis_status | 15 from model import analysis_status |
| 15 from model import result_status | 16 from model import result_status |
| 16 from model.wf_analysis import WfAnalysis | 17 from model.wf_analysis import WfAnalysis |
| 17 from model.wf_try_job import WfTryJob | 18 from model.wf_try_job import WfTryJob |
| 18 from model.wf_try_job_data import WfTryJobData | 19 from model.wf_try_job_data import WfTryJobData |
| 19 from waterfall.send_notification_for_culprit_pipeline import ( | 20 from waterfall.send_notification_for_culprit_pipeline import ( |
| 20 SendNotificationForCulpritPipeline) | 21 SendNotificationForCulpritPipeline) |
| 22 from waterfall import suspected_cl_util |
| 21 | 23 |
| 22 | 24 |
| 23 GIT_REPO = GitRepository( | 25 GIT_REPO = GitRepository( |
| 24 'https://chromium.googlesource.com/chromium/src.git', HttpClient()) | 26 'https://chromium.googlesource.com/chromium/src.git', HttpClient()) |
| 25 | 27 |
| 26 | 28 |
| 27 def _GetResultAnalysisStatus(analysis, result): | 29 def _GetResultAnalysisStatus(analysis, result): |
| 28 """Returns the analysis status based on existing status and try job result. | 30 """Returns the analysis status based on existing status and try job result. |
| 29 | 31 |
| 30 Args: | 32 Args: |
| (...skipping 13 matching lines...) Expand all Loading... |
| 44 if (try_job_found_culprit and | 46 if (try_job_found_culprit and |
| 45 (old_result_status is None or | 47 (old_result_status is None or |
| 46 old_result_status == result_status.NOT_FOUND_UNTRIAGED or | 48 old_result_status == result_status.NOT_FOUND_UNTRIAGED or |
| 47 old_result_status == result_status.NOT_FOUND_INCORRECT or | 49 old_result_status == result_status.NOT_FOUND_INCORRECT or |
| 48 old_result_status == result_status.NOT_FOUND_CORRECT)): | 50 old_result_status == result_status.NOT_FOUND_CORRECT)): |
| 49 return result_status.FOUND_UNTRIAGED | 51 return result_status.FOUND_UNTRIAGED |
| 50 | 52 |
| 51 return old_result_status | 53 return old_result_status |
| 52 | 54 |
| 53 | 55 |
| 54 def _GetSuspectedCLs(analysis, result): | 56 def _GetSuspectedCLs(analysis, culprits): |
| 55 """Returns a list of suspected CLs. | 57 """Returns a list of suspected CLs. |
| 56 | 58 |
| 57 Args: | 59 Args: |
| 58 analysis: The WfAnalysis entity corresponding to this try job. | 60 analysis: The WfAnalysis entity corresponding to this try job. |
| 59 result: A result dict containing the culprit from the results of | 61 culprits: A list of suspected CLs found by the try job. |
| 60 this try job. | |
| 61 | 62 |
| 62 Returns: | 63 Returns: |
| 63 A combined list of suspected CLs from those already in analysis and those | 64 A combined list of suspected CLs from those already in analysis and those |
| 64 found by this try job. | 65 found by this try job. |
| 65 """ | 66 """ |
| 66 suspected_cls = analysis.suspected_cls[:] if analysis.suspected_cls else [] | 67 suspected_cls = analysis.suspected_cls[:] if analysis.suspected_cls else [] |
| 67 suspected_cl_revisions = [cl['revision'] for cl in suspected_cls] | 68 suspected_cl_revisions = [cl['revision'] for cl in suspected_cls] |
| 68 culprit = result.get('culprit') | |
| 69 compile_cl_info = culprit.get('compile') | |
| 70 | 69 |
| 71 if compile_cl_info: | 70 for revision, try_job_suspected_cl in culprits.iteritems(): |
| 72 # Suspected CL is from compile failure. | |
| 73 revision = compile_cl_info.get('revision') | |
| 74 if revision not in suspected_cl_revisions: | 71 if revision not in suspected_cl_revisions: |
| 75 suspected_cl_revisions.append(revision) | 72 suspected_cl_revisions.append(revision) |
| 76 suspected_cls.append(compile_cl_info) | 73 suspected_cls.append(try_job_suspected_cl) |
| 77 return suspected_cls | |
| 78 | |
| 79 # Suspected CLs are from test failures. | |
| 80 for results in culprit.itervalues(): | |
| 81 if results.get('revision'): | |
| 82 # Non swarming test failures, only have step level failure info. | |
| 83 revision = results.get('revision') | |
| 84 cl_info = { | |
| 85 'url': results.get('url'), | |
| 86 'repo_name': results.get('repo_name'), | |
| 87 'revision': results.get('revision'), | |
| 88 'commit_position': results.get('commit_position') | |
| 89 } | |
| 90 if revision not in suspected_cl_revisions: | |
| 91 suspected_cl_revisions.append(revision) | |
| 92 suspected_cls.append(cl_info) | |
| 93 else: | |
| 94 for test_cl_info in results['tests'].values(): | |
| 95 revision = test_cl_info.get('revision') | |
| 96 if revision not in suspected_cl_revisions: | |
| 97 suspected_cl_revisions.append(revision) | |
| 98 suspected_cls.append(test_cl_info) | |
| 99 | 74 |
| 100 return suspected_cls | 75 return suspected_cls |
| 101 | 76 |
| 102 | 77 |
| 103 def _GetFailedRevisionFromResultsDict(results_dict): | 78 def _GetFailedRevisionFromResultsDict(results_dict): |
| 104 """Finds the failed revision from the given dict of revisions. | 79 """Finds the failed revision from the given dict of revisions. |
| 105 | 80 |
| 106 Args: | 81 Args: |
| 107 results_dict: (dict) A dict that maps revisions to their results. For | 82 results_dict: (dict) A dict that maps revisions to their results. For |
| 108 example: | 83 example: |
| (...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 161 if (not test_result['valid'] or | 136 if (not test_result['valid'] or |
| 162 test_result['status'] != 'failed'): # pragma: no cover | 137 test_result['status'] != 'failed'): # pragma: no cover |
| 163 continue | 138 continue |
| 164 | 139 |
| 165 failed_revisions.add(revision) | 140 failed_revisions.add(revision) |
| 166 | 141 |
| 167 if step not in culprit_map: | 142 if step not in culprit_map: |
| 168 culprit_map[step] = { | 143 culprit_map[step] = { |
| 169 'tests': {} | 144 'tests': {} |
| 170 } | 145 } |
| 171 if (not test_result['failures'] and | |
| 172 not culprit_map[step].get('revision')): | |
| 173 # Non swarming test failures, only have step level failure info. | |
| 174 culprit_map[step]['revision'] = revision | |
| 175 for failed_test in test_result['failures']: | 146 for failed_test in test_result['failures']: |
| 176 # Swarming tests, gets first failed revision for each test. | 147 # Swarming tests, gets first failed revision for each test. |
| 177 if failed_test not in culprit_map[step]['tests']: | 148 if failed_test not in culprit_map[step]['tests']: |
| 178 culprit_map[step]['tests'][failed_test] = { | 149 culprit_map[step]['tests'][failed_test] = { |
| 179 'revision': revision | 150 'revision': revision |
| 180 } | 151 } |
| 181 | 152 |
| 182 return culprit_map, list(failed_revisions) | 153 return culprit_map, list(failed_revisions) |
| 183 | 154 |
| 184 | 155 |
| (...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 234 send_notification_right_now) | 205 send_notification_right_now) |
| 235 elif compile_suspected_cl: | 206 elif compile_suspected_cl: |
| 236 # A special case where try job didn't find any suspected cls, but | 207 # A special case where try job didn't find any suspected cls, but |
| 237 # heuristic found a suspected_cl. | 208 # heuristic found a suspected_cl. |
| 238 _StartSendNotificationPipeline( | 209 _StartSendNotificationPipeline( |
| 239 master_name, builder_name, build_number, | 210 master_name, builder_name, build_number, |
| 240 compile_suspected_cl['repo_name'], compile_suspected_cl['revision'], | 211 compile_suspected_cl['repo_name'], compile_suspected_cl['revision'], |
| 241 send_notification_right_now=True) | 212 send_notification_right_now=True) |
| 242 | 213 |
| 243 | 214 |
| 215 def _GetTestFailureCausedByCL(result): |
| 216 if not result: |
| 217 return None |
| 218 |
| 219 failures = {} |
| 220 for step_name, step_result in result.iteritems(): |
| 221 if step_result['status'] == 'failed': |
| 222 failures[step_name] = step_result['failures'] |
| 223 |
| 224 return failures |
| 225 |
| 226 |
| 244 class IdentifyTryJobCulpritPipeline(BasePipeline): | 227 class IdentifyTryJobCulpritPipeline(BasePipeline): |
| 245 """A pipeline to identify culprit CL info based on try job compile results.""" | 228 """A pipeline to identify culprit CL info based on try job compile results.""" |
| 246 | 229 |
| 247 def _GetCulpritInfo(self, failed_revisions): | 230 def _GetCulpritInfo(self, failed_revisions): |
| 248 """Gets commit_positions and review urls for revisions.""" | 231 """Gets commit_positions and review urls for revisions.""" |
| 249 culprits = {} | 232 culprits = {} |
| 250 # TODO(lijeffrey): remove hard-coded 'chromium' when DEPS file parsing is | 233 # TODO(lijeffrey): remove hard-coded 'chromium' when DEPS file parsing is |
| 251 # supported. | 234 # supported. |
| 252 for failed_revision in failed_revisions: | 235 for failed_revision in failed_revisions: |
| 253 culprits[failed_revision] = { | 236 culprits[failed_revision] = { |
| (...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 288 } | 271 } |
| 289 failed_revisions.add(revision) | 272 failed_revisions.add(revision) |
| 290 return culprit_map, list(failed_revisions) | 273 return culprit_map, list(failed_revisions) |
| 291 | 274 |
| 292 return _GetCulpritsForTestsFromResultsDict( | 275 return _GetCulpritsForTestsFromResultsDict( |
| 293 blame_list, result['report'].get('result')) | 276 blame_list, result['report'].get('result')) |
| 294 | 277 |
| 295 def _UpdateCulpritMapWithCulpritInfo(self, culprit_map, culprits): | 278 def _UpdateCulpritMapWithCulpritInfo(self, culprit_map, culprits): |
| 296 """Fills in commit_position and review url for each failed rev in map.""" | 279 """Fills in commit_position and review url for each failed rev in map.""" |
| 297 for step_culprit in culprit_map.values(): | 280 for step_culprit in culprit_map.values(): |
| 298 if step_culprit.get('revision'): | |
| 299 culprit = culprits[step_culprit['revision']] | |
| 300 step_culprit['commit_position'] = culprit['commit_position'] | |
| 301 step_culprit['url'] = culprit['url'] | |
| 302 step_culprit['repo_name'] = culprit['repo_name'] | |
| 303 for test_culprit in step_culprit.get('tests', {}).values(): | 281 for test_culprit in step_culprit.get('tests', {}).values(): |
| 304 test_revision = test_culprit['revision'] | 282 test_revision = test_culprit['revision'] |
| 305 test_culprit.update(culprits[test_revision]) | 283 test_culprit.update(culprits[test_revision]) |
| 306 | 284 |
| 307 def _GetCulpritDataForTest(self, culprit_map): | 285 def _GetCulpritDataForTest(self, culprit_map): |
| 308 """Gets culprit revision for each failure for try job metadata.""" | 286 """Gets culprit revision for each failure for try job metadata.""" |
| 309 culprit_data = {} | 287 culprit_data = {} |
| 310 for step, step_culprit in culprit_map.iteritems(): | 288 for step, step_culprit in culprit_map.iteritems(): |
| 311 if step_culprit['tests']: | 289 culprit_data[step] = {} |
| 312 culprit_data[step] = {} | 290 for test, test_culprit in step_culprit['tests'].iteritems(): |
| 313 for test, test_culprit in step_culprit['tests'].iteritems(): | 291 culprit_data[step][test] = test_culprit['revision'] |
| 314 culprit_data[step][test] = test_culprit['revision'] | |
| 315 else: | |
| 316 culprit_data[step] = step_culprit['revision'] | |
| 317 return culprit_data | 292 return culprit_data |
| 318 | 293 |
| 319 # Arguments number differs from overridden method - pylint: disable=W0221 | 294 # Arguments number differs from overridden method - pylint: disable=W0221 |
| 320 def run( | 295 def run( |
| 321 self, master_name, builder_name, build_number, blame_list, try_job_type, | 296 self, master_name, builder_name, build_number, blame_list, try_job_type, |
| 322 try_job_id, result): | 297 try_job_id, result): |
| 323 """Identifies the information for failed revisions. | 298 """Identifies the information for failed revisions. |
| 324 | 299 |
| 325 Please refer to try_job_result_format.md for format check. | 300 Please refer to try_job_result_format.md for format check. |
| 326 """ | 301 """ |
| (...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 366 | 341 |
| 367 @ndb.transactional | 342 @ndb.transactional |
| 368 def UpdateWfAnalysisWithTryJobResult(): | 343 def UpdateWfAnalysisWithTryJobResult(): |
| 369 if not culprits: | 344 if not culprits: |
| 370 return | 345 return |
| 371 | 346 |
| 372 analysis = WfAnalysis.Get(master_name, builder_name, build_number) | 347 analysis = WfAnalysis.Get(master_name, builder_name, build_number) |
| 373 # Update analysis result and suspected CLs with results of this try job if | 348 # Update analysis result and suspected CLs with results of this try job if |
| 374 # culprits were found. | 349 # culprits were found. |
| 375 updated_result_status = _GetResultAnalysisStatus(analysis, result) | 350 updated_result_status = _GetResultAnalysisStatus(analysis, result) |
| 376 updated_suspected_cls = _GetSuspectedCLs(analysis, result) | 351 updated_suspected_cls = _GetSuspectedCLs(analysis, culprits) |
| 377 | |
| 378 if (analysis.result_status != updated_result_status or | 352 if (analysis.result_status != updated_result_status or |
| 379 analysis.suspected_cls != updated_suspected_cls): | 353 analysis.suspected_cls != updated_suspected_cls): |
| 380 analysis.result_status = updated_result_status | 354 analysis.result_status = updated_result_status |
| 381 analysis.suspected_cls = updated_suspected_cls | 355 analysis.suspected_cls = updated_suspected_cls |
| 382 analysis.put() | 356 analysis.put() |
| 383 | 357 |
| 358 def UpdateSuspectedCLs(): |
| 359 if not culprits: |
| 360 return |
| 361 |
| 362 # Creates or updates each suspected_cl. |
| 363 for culprit in culprits.values(): |
| 364 revision = culprit['revision'] |
| 365 if try_job_type == failure_type.COMPILE: |
| 366 failures = {'compile': []} |
| 367 else: |
| 368 failures = _GetTestFailureCausedByCL( |
| 369 result.get('report', {}).get('result', {}).get(revision)) |
| 370 |
| 371 suspected_cl_util.UpdateSuspectedCL( |
| 372 culprit['repo_name'], revision, culprit.get('commit_position'), |
| 373 analysis_approach_type.TRY_JOB, master_name, builder_name, |
| 374 build_number, try_job_type, failures, None) |
| 375 |
| 376 # try_job_suspected_cls = _GetTryJobSuspectedCLs(result) |
| 384 # Store try-job results. | 377 # Store try-job results. |
| 385 UpdateTryJobResult() | 378 UpdateTryJobResult() |
| 386 | 379 |
| 387 # Saves cls found by heuristic approach for later use. | 380 # Saves cls found by heuristic approach for later use. |
| 388 # This part must be before UpdateWfAnalysisWithTryJobResult(). | 381 # This part must be before UpdateWfAnalysisWithTryJobResult(). |
| 389 analysis = WfAnalysis.Get(master_name, builder_name, build_number) | 382 analysis = WfAnalysis.Get(master_name, builder_name, build_number) |
| 390 heuristic_cls = _GetHeuristicSuspectedCLs(analysis) | 383 heuristic_cls = _GetHeuristicSuspectedCLs(analysis) |
| 391 compile_suspected_cl = ( | 384 compile_suspected_cl = ( |
| 392 _GetSuspectedCLFoundByHeuristicForCompile(analysis) | 385 _GetSuspectedCLFoundByHeuristicForCompile(analysis) |
| 393 if try_job_type == failure_type.COMPILE else None) | 386 if try_job_type == failure_type.COMPILE else None) |
| 394 | 387 |
| 395 # Add try-job results to WfAnalysis. | 388 # Add try-job results to WfAnalysis. |
| 396 UpdateWfAnalysisWithTryJobResult() | 389 UpdateWfAnalysisWithTryJobResult() |
| 390 # Updates suspected_cl. |
| 391 UpdateSuspectedCLs() |
| 397 | 392 |
| 398 _NotifyCulprits(master_name, builder_name, build_number, culprits, | 393 _NotifyCulprits(master_name, builder_name, build_number, culprits, |
| 399 heuristic_cls, compile_suspected_cl) | 394 heuristic_cls, compile_suspected_cl) |
| 400 return result.get('culprit') if result else None | 395 return result.get('culprit') if result else None |
| OLD | NEW |