| 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 import collections | 5 from collections import defaultdict |
| 6 import os | 6 import os |
| 7 import re | 7 import re |
| 8 | 8 |
| 9 from common.diff import ChangeType | 9 from common.diff import ChangeType |
| 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 waterfall import waterfall_config | 12 from waterfall import waterfall_config |
| 13 from waterfall.failure_signal import FailureSignal | 13 from waterfall.failure_signal import FailureSignal |
| 14 | 14 |
| 15 | 15 |
| (...skipping 210 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 226 appearing in the compile failure. | 226 appearing in the compile failure. |
| 227 2) If a hint shows that a CL is likely-suspected, the hint is given 1 | 227 2) If a hint shows that a CL is likely-suspected, the hint is given 1 |
| 228 score point. Eg. a CL is just likely suspected if it only changed a | 228 score point. Eg. a CL is just likely suspected if it only changed a |
| 229 related file (x_impl.cc vs. x.h) appearing in a failure. | 229 related file (x_impl.cc vs. x.h) appearing in a failure. |
| 230 2. hints: each hint is a string describing a reason for suspecting a CL and | 230 2. hints: each hint is a string describing a reason for suspecting a CL and |
| 231 could be shown to the user (eg., "added x_impl.cc (and it was in log)"). | 231 could be shown to the user (eg., "added x_impl.cc (and it was in log)"). |
| 232 """ | 232 """ |
| 233 | 233 |
| 234 def __init__(self): | 234 def __init__(self): |
| 235 self._score = 0 | 235 self._score = 0 |
| 236 self._hints = collections.defaultdict(int) | 236 self._hints = defaultdict(int) |
| 237 | 237 |
| 238 @property | 238 @property |
| 239 def score(self): | 239 def score(self): |
| 240 return self._score | 240 return self._score |
| 241 | 241 |
| 242 def AddFileChange(self, change_action, changed_src_file_path, | 242 def AddFileChange(self, change_action, changed_src_file_path, |
| 243 file_path_in_log, score, num_file_name_occurrences, | 243 file_path_in_log, score, num_file_name_occurrences, |
| 244 changed_line_numbers=None): | 244 changed_line_numbers=None): |
| 245 """Adds a suspected file change. | 245 """Adds a suspected file change. |
| 246 | 246 |
| (...skipping 311 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 558 failure_signal (FailureSignal): The failure signal of a failed step or test. | 558 failure_signal (FailureSignal): The failure signal of a failed step or test. |
| 559 change_log (dict): The change log of a CL as returned by | 559 change_log (dict): The change log of a CL as returned by |
| 560 common.change_log.ChangeLog.ToDict(). | 560 common.change_log.ChangeLog.ToDict(). |
| 561 deps_info (dict): Output of pipeline ExtractDEPSInfoPipeline. | 561 deps_info (dict): Output of pipeline ExtractDEPSInfoPipeline. |
| 562 | 562 |
| 563 Returns: | 563 Returns: |
| 564 A dict as returned by _Justification.ToDict() if the CL is suspected for the | 564 A dict as returned by _Justification.ToDict() if the CL is suspected for the |
| 565 failure; otherwise None. | 565 failure; otherwise None. |
| 566 """ | 566 """ |
| 567 # Use a dict to map each file name of the touched files to their occurrences. | 567 # Use a dict to map each file name of the touched files to their occurrences. |
| 568 file_name_occurrences = collections.defaultdict(int) | 568 file_name_occurrences = defaultdict(int) |
| 569 for touched_file in change_log['touched_files']: | 569 for touched_file in change_log['touched_files']: |
| 570 change_type = touched_file['change_type'] | 570 change_type = touched_file['change_type'] |
| 571 if (change_type in (ChangeType.ADD, ChangeType.COPY, | 571 if (change_type in (ChangeType.ADD, ChangeType.COPY, |
| 572 ChangeType.RENAME, ChangeType.MODIFY)): | 572 ChangeType.RENAME, ChangeType.MODIFY)): |
| 573 file_name = os.path.basename(touched_file['new_path']) | 573 file_name = os.path.basename(touched_file['new_path']) |
| 574 file_name_occurrences[file_name] += 1 | 574 file_name_occurrences[file_name] += 1 |
| 575 | 575 |
| 576 if change_type in (ChangeType.DELETE, ChangeType.RENAME): | 576 if change_type in (ChangeType.DELETE, ChangeType.RENAME): |
| 577 file_name = os.path.basename(touched_file['old_path']) | 577 file_name = os.path.basename(touched_file['old_path']) |
| 578 file_name_occurrences[file_name] += 1 | 578 file_name_occurrences[file_name] += 1 |
| (...skipping 14 matching lines...) Expand all Loading... |
| 593 | 593 |
| 594 _CheckFileInDependencyRolls(file_path_in_log, rolls, justification, | 594 _CheckFileInDependencyRolls(file_path_in_log, rolls, justification, |
| 595 line_numbers) | 595 line_numbers) |
| 596 | 596 |
| 597 if not justification.score: | 597 if not justification.score: |
| 598 return None | 598 return None |
| 599 else: | 599 else: |
| 600 return justification.ToDict() | 600 return justification.ToDict() |
| 601 | 601 |
| 602 | 602 |
| 603 class _CLInfo(object): |
| 604 """A object of information we need for a suspected CL. |
| 605 |
| 606 The information is specific to current build. |
| 607 """ |
| 608 def __init__(self): |
| 609 self.failures = defaultdict(list) |
| 610 self.top_score = 0 |
| 611 self.url = None |
| 612 |
| 613 |
| 614 def _SaveFailureToMap( |
| 615 cl_failure_map, new_suspected_cl_dict, step_name, test_name, top_score): |
| 616 """Saves a failure's info to the cl that caused it.""" |
| 617 cl_key = ( |
| 618 new_suspected_cl_dict['repo_name'], new_suspected_cl_dict['revision'], |
| 619 new_suspected_cl_dict['commit_position']) |
| 620 |
| 621 if test_name: |
| 622 cl_failure_map[cl_key].failures[step_name].append(test_name) |
| 623 else: |
| 624 cl_failure_map[cl_key].failures[step_name] = [] |
| 625 # Ignores the case where in the same build for the same cl, |
| 626 # we have different scores. |
| 627 # Not sure if we need to handle it since it should be rare. |
| 628 cl_failure_map[cl_key].top_score = ( |
| 629 cl_failure_map[cl_key].top_score or top_score) |
| 630 cl_failure_map[cl_key].url = ( |
| 631 cl_failure_map[cl_key].url or new_suspected_cl_dict['url']) |
| 632 |
| 633 |
| 634 def _ConvertCLFailureMapToList(cl_failure_map): |
| 635 suspected_cls = [] |
| 636 for cl_key, cl_info in cl_failure_map.iteritems(): |
| 637 suspected_cl = {} |
| 638 (suspected_cl['repo_name'], suspected_cl['revision'], |
| 639 suspected_cl['commit_position']) = cl_key |
| 640 suspected_cl['url'] = cl_info.url |
| 641 suspected_cl['failures'] = cl_info.failures |
| 642 suspected_cl['top_score'] = cl_info.top_score |
| 643 |
| 644 suspected_cls.append(suspected_cl) |
| 645 return suspected_cls |
| 646 |
| 603 def AnalyzeBuildFailure( | 647 def AnalyzeBuildFailure( |
| 604 failure_info, change_logs, deps_info, failure_signals): | 648 failure_info, change_logs, deps_info, failure_signals): |
| 605 """Analyze the given failure signals, and figure out culprit CLs. | 649 """Analyzes the given failure signals, and figure out culprit CLs. |
| 606 | 650 |
| 607 Args: | 651 Args: |
| 608 failure_info (dict): Output of pipeline DetectFirstFailurePipeline. | 652 failure_info (dict): Output of pipeline DetectFirstFailurePipeline. |
| 609 change_logs (dict): Output of pipeline PullChangelogPipeline. | 653 change_logs (dict): Output of pipeline PullChangelogPipeline. |
| 610 deps_info (dict): Output of pipeline ExtractDEPSInfoPipeline. | 654 deps_info (dict): Output of pipeline ExtractDEPSInfoPipeline. |
| 611 failure_signals (dict): Output of pipeline ExtractSignalPipeline. | 655 failure_signals (dict): Output of pipeline ExtractSignalPipeline. |
| 612 | 656 |
| 613 Returns: | 657 Returns: |
| 614 A dict with the following form: | 658 A dict with the following form: |
| 615 { | 659 { |
| (...skipping 16 matching lines...) Expand all Loading... |
| 632 'modify e/f/z.cc': 1, | 676 'modify e/f/z.cc': 1, |
| 633 ... | 677 ... |
| 634 } | 678 } |
| 635 }, | 679 }, |
| 636 ... | 680 ... |
| 637 ], | 681 ], |
| 638 }, | 682 }, |
| 639 ... | 683 ... |
| 640 ] | 684 ] |
| 641 } | 685 } |
| 686 |
| 687 And a list of suspected_cls format as below: |
| 688 [ |
| 689 { |
| 690 'repo_name': 'chromium', |
| 691 'revision': 'r98_1', |
| 692 'commit_position': None, |
| 693 'url': None, |
| 694 'failures': { |
| 695 'b': ['Unittest2.Subtest1', 'Unittest3.Subtest2'] |
| 696 }, |
| 697 'top_score': 4 |
| 698 }, |
| 699 ... |
| 700 ] |
| 642 """ | 701 """ |
| 643 analysis_result = { | 702 analysis_result = { |
| 644 'failures': [] | 703 'failures': [] |
| 645 } | 704 } |
| 646 | 705 |
| 647 if not failure_info['failed'] or not failure_info['chromium_revision']: | 706 if not failure_info['failed'] or not failure_info['chromium_revision']: |
| 648 # Bail out if no failed step or no chromium revision. | 707 # Bail out if no failed step or no chromium revision. |
| 649 return analysis_result | 708 return analysis_result, [] |
| 650 | 709 |
| 651 def CreateCLInfoDict(justification_dict, build_number, change_log): | 710 def CreateCLInfoDict(justification_dict, build_number, change_log): |
| 652 # TODO(stgao): remove hard-coded 'chromium' when DEPS file parsing is | 711 # TODO(stgao): remove hard-coded 'chromium' when DEPS file parsing is |
| 653 # supported. | 712 # supported. |
| 654 cl_info = { | 713 cl_info = { |
| 655 'build_number': build_number, | 714 'build_number': build_number, |
| 656 'repo_name': 'chromium', | 715 'repo_name': 'chromium', |
| 657 'revision': change_log['revision'], | 716 'revision': change_log['revision'], |
| 658 'commit_position': change_log.get('commit_position'), | 717 'commit_position': change_log.get('commit_position'), |
| 659 'url': | 718 'url': |
| 660 change_log.get('code_review_url') or change_log.get('commit_url'), | 719 change_log.get('code_review_url') or change_log.get('commit_url'), |
| 661 } | 720 } |
| 662 | 721 |
| 663 cl_info.update(justification_dict) | 722 cl_info.update(justification_dict) |
| 664 return cl_info | 723 return cl_info |
| 665 | 724 |
| 666 failed_steps = failure_info['failed_steps'] | 725 failed_steps = failure_info['failed_steps'] |
| 667 builds = failure_info['builds'] | 726 builds = failure_info['builds'] |
| 668 master_name = failure_info.get('master_name') | 727 master_name = failure_info['master_name'] |
| 728 |
| 729 cl_failure_map = defaultdict(_CLInfo) |
| 669 | 730 |
| 670 for step_name, step_failure_info in failed_steps.iteritems(): | 731 for step_name, step_failure_info in failed_steps.iteritems(): |
| 671 is_test_level = step_failure_info.get('tests') is not None | 732 is_test_level = step_failure_info.get('tests') is not None |
| 672 | 733 |
| 673 failed_build_number = step_failure_info['current_failure'] | 734 failed_build_number = step_failure_info['current_failure'] |
| 674 if step_failure_info.get('last_pass') is not None: | 735 if step_failure_info.get('last_pass') is not None: |
| 675 start_build_number = step_failure_info.get('last_pass') + 1 | 736 start_build_number = step_failure_info.get('last_pass') + 1 |
| 676 else: | 737 else: |
| 677 start_build_number = step_failure_info['first_failure'] | 738 start_build_number = step_failure_info['first_failure'] |
| 678 step_analysis_result = { | 739 step_analysis_result = { |
| (...skipping 30 matching lines...) Expand all Loading... |
| 709 test_signal, change_logs[revision], deps_info) | 770 test_signal, change_logs[revision], deps_info) |
| 710 | 771 |
| 711 if not justification_dict: | 772 if not justification_dict: |
| 712 continue | 773 continue |
| 713 | 774 |
| 714 new_suspected_cl_dict = CreateCLInfoDict( | 775 new_suspected_cl_dict = CreateCLInfoDict( |
| 715 justification_dict, build_number, change_logs[revision]) | 776 justification_dict, build_number, change_logs[revision]) |
| 716 test_analysis_result['suspected_cls'].append( | 777 test_analysis_result['suspected_cls'].append( |
| 717 new_suspected_cl_dict) | 778 new_suspected_cl_dict) |
| 718 | 779 |
| 780 _SaveFailureToMap( |
| 781 cl_failure_map, new_suspected_cl_dict, step_name, test_name, |
| 782 max(justification_dict['hints'].values())) |
| 783 |
| 719 # Checks Files on step level using step level signals | 784 # Checks Files on step level using step level signals |
| 720 # regardless of test level signals so we can make sure | 785 # regardless of test level signals so we can make sure |
| 721 # no duplicate justifications added to the step result. | 786 # no duplicate justifications added to the step result. |
| 722 failure_signal = FailureSignal.FromDict(failure_signals[step_name]) | 787 failure_signal = FailureSignal.FromDict(failure_signals[step_name]) |
| 723 justification_dict = _CheckFiles( | 788 justification_dict = _CheckFiles( |
| 724 failure_signal, change_logs[revision], deps_info) | 789 failure_signal, change_logs[revision], deps_info) |
| 725 | 790 |
| 726 if not justification_dict: | 791 if not justification_dict: |
| 727 continue | 792 continue |
| 728 | 793 |
| 729 step_analysis_result['suspected_cls'].append( | 794 new_suspected_cl_dict = CreateCLInfoDict( |
| 730 CreateCLInfoDict(justification_dict, build_number, | 795 justification_dict, build_number, change_logs[revision]) |
| 731 change_logs[revision])) | 796 step_analysis_result['suspected_cls'].append(new_suspected_cl_dict) |
| 797 |
| 798 if not is_test_level: |
| 799 _SaveFailureToMap( |
| 800 cl_failure_map, new_suspected_cl_dict, step_name, None, |
| 801 max(justification_dict['hints'].values())) |
| 732 | 802 |
| 733 # TODO(stgao): sort CLs by score. | 803 # TODO(stgao): sort CLs by score. |
| 734 analysis_result['failures'].append(step_analysis_result) | 804 analysis_result['failures'].append(step_analysis_result) |
| 735 | 805 |
| 736 return analysis_result | 806 suspected_cls = _ConvertCLFailureMapToList(cl_failure_map) |
| 807 |
| 808 return analysis_result, suspected_cls |
| OLD | NEW |