| OLD | NEW |
| (Empty) |
| 1 # Copyright 2016 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 from collections import namedtuple | |
| 6 | |
| 7 | |
| 8 # TODO(wrengr): we should change things to use integers with None as | |
| 9 # \"infinity\", rather than using floats. | |
| 10 # TODO(http://crbug.com/644476): this class needs a better name. | |
| 11 class AnalysisInfo(namedtuple('AnalysisInfo', | |
| 12 ['min_distance', 'min_distance_frame'])): | |
| 13 __slots__ = () | |
| 14 | |
| 15 def __str__(self): # pragma: no cover | |
| 16 return ('AnalysisInfo(min_distance = %d, min_distance_frame = %s)' | |
| 17 % (self.min_distance, self.min_distance_frame)) | |
| 18 | |
| 19 | |
| 20 # TODO(wrengr): it's not clear why the ``priority`` is stored at all, | |
| 21 # given that every use in this file discards it. ``Result.file_to_stack_infos`` | |
| 22 # should just store pointers directly to the frames themselves rather | |
| 23 # than needing this intermediate object. | |
| 24 # TODO(http://crbug.com/644476): this class needs a better name. | |
| 25 class StackInfo(namedtuple('StackInfo', ['frame', 'priority'])): | |
| 26 """Pair of a frame and the ``priority`` of the ``CallStack`` it came from.""" | |
| 27 __slots__ = () | |
| 28 | |
| 29 def __str__(self): # pragma: no cover | |
| 30 return 'StackInfo(frame = %s, priority = %f)' % (self.frame, self.priority) | |
| 31 | |
| 32 | |
| 33 # TODO(http://crbug.com/644476): this class needs a better name. | |
| 34 class Result(object): | |
| 35 """Represents findit culprit result.""" | |
| 36 | |
| 37 def __init__(self, changelog, dep_path, | |
| 38 confidence=None, reasons=None, changed_files=None): | |
| 39 assert isinstance(confidence, (int, float, type(None))), TypeError( | |
| 40 'In the ``confidence`` argument to the Result constructor, ' | |
| 41 'expected a number or None, but got a %s object instead.' | |
| 42 % confidence.__class__.__name__) | |
| 43 self.changelog = changelog | |
| 44 self.dep_path = dep_path | |
| 45 self.confidence = None if confidence is None else float(confidence) | |
| 46 self.reasons = reasons | |
| 47 self.changed_files = changed_files | |
| 48 | |
| 49 # TODO(wrengr): (a) make these two fields private/readonly | |
| 50 # TODO(wrengr): (b) zip them together. | |
| 51 self.file_to_stack_infos = {} | |
| 52 self.file_to_analysis_info = {} | |
| 53 | |
| 54 def ToDict(self): | |
| 55 return { | |
| 56 'url': self.changelog.commit_url, | |
| 57 'review_url': self.changelog.code_review_url, | |
| 58 'revision': self.changelog.revision, | |
| 59 'project_path': self.dep_path, | |
| 60 'author': self.changelog.author_email, | |
| 61 'time': str(self.changelog.author_time), | |
| 62 'reasons': self.reasons, | |
| 63 'changed_files': self.changed_files, | |
| 64 'confidence': self.confidence, | |
| 65 } | |
| 66 | |
| 67 # TODO(katesonia): This is unusable for logging because in all the | |
| 68 # cases that need logging it returns the empty string! We should print | |
| 69 # this out in a more useful way (e.g., how CrashConfig is printed) | |
| 70 # so that callers don't have to use ``str(result.ToDict())`` instead. If | |
| 71 # we want a method that does what this one does, we should give it a | |
| 72 # different name that indicates what it's actually printing out. | |
| 73 def ToString(self): | |
| 74 if not self.file_to_stack_infos: | |
| 75 return '' | |
| 76 | |
| 77 lines = [] | |
| 78 for file_path, stack_infos in self.file_to_stack_infos.iteritems(): | |
| 79 line_parts = [] | |
| 80 for frame, _ in stack_infos: | |
| 81 line_parts.append('frame #%d' % frame.index) | |
| 82 | |
| 83 lines.append('Changed file %s crashed in %s' % ( | |
| 84 file_path, ', '.join(line_parts))) | |
| 85 | |
| 86 return '\n'.join(lines) | |
| 87 | |
| 88 def __str__(self): | |
| 89 return self.ToString() | |
| 90 | |
| 91 | |
| 92 class MatchResult(Result): | |
| 93 """Represents findit culprit result got from match algorithm.""" | |
| 94 | |
| 95 def Update(self, file_path, stack_infos, blame): | |
| 96 """Updates a match result with file path and its stack_infos and blame. | |
| 97 | |
| 98 When a file_path is found both shown in stacktrace and touched by | |
| 99 the revision of this result, update result with the information of | |
| 100 this file. | |
| 101 | |
| 102 Inserts the file path and its stack infos, and updates the min distance | |
| 103 if less distance is found between touched lines of this result and | |
| 104 crashed lines in the file path. | |
| 105 | |
| 106 Args: | |
| 107 file_path (str): File path of the crashed file. | |
| 108 stack_infos (list of StackInfo): List of the frames of this file | |
| 109 together with their callstack priorities. | |
| 110 blame (Blame): Blame oject of this file. | |
| 111 """ | |
| 112 self.file_to_stack_infos[file_path] = stack_infos | |
| 113 | |
| 114 if not blame: | |
| 115 return | |
| 116 | |
| 117 min_distance = float('inf') | |
| 118 min_distance_frame = stack_infos[0][0] | |
| 119 for region in blame: | |
| 120 if region.revision != self.changelog.revision: | |
| 121 continue | |
| 122 | |
| 123 region_start = region.start | |
| 124 region_end = region_start + region.count - 1 | |
| 125 for frame, _ in stack_infos: | |
| 126 frame_start = frame.crashed_line_numbers[0] | |
| 127 frame_end = frame.crashed_line_numbers[-1] | |
| 128 distance = _DistanceBetweenLineRanges((frame_start, frame_end), | |
| 129 (region_start, region_end)) | |
| 130 if distance < min_distance: | |
| 131 min_distance = distance | |
| 132 min_distance_frame = frame | |
| 133 | |
| 134 self.file_to_analysis_info[file_path] = AnalysisInfo( | |
| 135 min_distance = min_distance, | |
| 136 min_distance_frame = min_distance_frame, | |
| 137 ) | |
| 138 | |
| 139 | |
| 140 def _DistanceBetweenLineRanges((start1, end1), (start2, end2)): | |
| 141 """Given two ranges, compute the (unsigned) distance between them. | |
| 142 | |
| 143 Args: | |
| 144 start1: the start of the first range | |
| 145 end1: the end of the first range. Must be greater than start1. | |
| 146 start2: the start of the second range | |
| 147 end2: the end of the second range. Must be greater than start2. | |
| 148 | |
| 149 Returns: | |
| 150 If the end of the earlier range comes before the start of the later | |
| 151 range, then the difference between those points. Otherwise, returns | |
| 152 zero (because the ranges overlap).""" | |
| 153 assert end1 >= start1, ValueError( | |
| 154 'the first range is empty: %d < %d' % (end1, start1)) | |
| 155 assert end2 >= start2, ValueError( | |
| 156 'the second range is empty: %d < %d' % (end2, start2)) | |
| 157 # There are six possible cases, but in all the cases where the two | |
| 158 # ranges overlap, the latter two differences will be negative. | |
| 159 return max(0, start2 - end1, start1 - end2) | |
| 160 | |
| 161 | |
| 162 class MatchResults(dict): | |
| 163 """A map from revisions to the MatchResult object for that revision.""" | |
| 164 | |
| 165 def __init__(self, ignore_cls=None): | |
| 166 super(MatchResults, self).__init__() | |
| 167 self._ignore_cls = ignore_cls | |
| 168 | |
| 169 def GenerateMatchResults(self, file_path, dep_path, | |
| 170 stack_infos, changelogs, blame): | |
| 171 """Compute match results from a list of CLs, and store them. | |
| 172 | |
| 173 Match results are generated based on newly found file path, its stack_infos, | |
| 174 and all the changelogs that touched this file in the dep in regression | |
| 175 ranges, those reverted changelogs should be ignored. | |
| 176 | |
| 177 Args: | |
| 178 file_path (str): File path of the crashed file. | |
| 179 dep_path (str): Path of the dependency of the file. | |
| 180 stack_infos (list): List of stack_info dicts, represents frames of this | |
| 181 file and the callstack priorities of those frames. | |
| 182 changelogs (list): List of Changelog objects in the dep in regression | |
| 183 range which touched the file. | |
| 184 blame (Blame): Blame of the file. | |
| 185 """ | |
| 186 for changelog in changelogs: | |
| 187 if self._ignore_cls and changelog.revision in self._ignore_cls: | |
| 188 continue | |
| 189 | |
| 190 if changelog.revision not in self: | |
| 191 self[changelog.revision] = MatchResult(changelog, dep_path) | |
| 192 | |
| 193 match_result = self[changelog.revision] | |
| 194 match_result.Update(file_path, stack_infos, blame) | |
| OLD | NEW |