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