Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 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 | |
| 3 # found in the LICENSE file. | |
| 4 | |
| 5 import json | |
| 6 import collections | |
| 7 import operator | |
| 8 | |
| 9 | |
| 10 # Git ready, but only implemented for SVN atm. | |
|
ojan
2014/07/22 02:01:23
I think we should only operate on monotonically in
| |
| 11 def ids_after_first_including_second(first, second): | |
| 12 if not first or not second: | |
| 13 return [] | |
| 14 try: | |
| 15 return range(int(first) + 1, int(second) + 1) | |
| 16 except ValueError, e: | |
| 17 # likely passed a git hash | |
| 18 return [] | |
| 19 | |
| 20 | |
| 21 # Git ready, but only implemented for SVN atm. | |
| 22 def is_ancestor_of(older, younger): | |
| 23 return int(older) < int(younger) | |
| 24 | |
| 25 | |
| 26 def is_decendant_of(younger, older): | |
| 27 return is_ancestor_of(older, younger) | |
| 28 | |
| 29 | |
| 30 def commit_compare(one, two): | |
| 31 if is_decendant_of(one, two): | |
| 32 return -1 | |
| 33 if is_ancestor_of(one, two): | |
| 34 return 1 | |
| 35 return 0 # This is technically not right, since commits can be non-comparabl e. | |
| 36 | |
| 37 | |
| 38 def flatten_to_commit_list(passing, failing): | |
| 39 # Flatten two commit dicts to a list of 'name:commit' | |
| 40 if not passing or not failing: | |
| 41 return [] | |
| 42 all_commits = [] | |
| 43 for name in passing.keys(): | |
| 44 commits = ids_after_first_including_second(passing[name], failing[name]) | |
| 45 all_commits.extend(['%s:%s' % (name, commit) for commit in commits]) | |
| 46 return all_commits | |
| 47 | |
| 48 | |
| 49 # FIXME: Perhaps this should be done by the feeder? | |
| 50 def assign_keys(alerts): | |
| 51 for key, alert in enumerate(alerts): | |
| 52 # We could come up with something more sophisticated if necessary. | |
| 53 alert['key'] = 'f%s' % key # Just something so it doesn't look like a nu mber. | |
| 54 return alerts | |
| 55 | |
| 56 | |
| 57 def _make_merge_dicts(reducer): | |
| 58 def merge_dicts(one, two): | |
| 59 if not one or not two: | |
| 60 return None | |
| 61 reduction = {} | |
| 62 for key in set(one.keys() + two.keys()): | |
| 63 one_value = one.get(key) | |
| 64 two_value = two.get(key) | |
| 65 reduction[key] = reducer(one_value, two_value) | |
| 66 return reduction | |
| 67 return merge_dicts | |
| 68 | |
| 69 | |
| 70 def merge_regression_ranges(alerts): | |
| 71 def make_compare(compare_func): | |
| 72 def compare(one, two): | |
| 73 if one and two and compare_func(one, two): | |
| 74 return one | |
| 75 # Treat None is 'infinite' so always return the more limiting. | |
| 76 # FIXME: 'passing' may wish to prefer None over a value? | |
| 77 return two or one | |
| 78 return compare | |
| 79 | |
| 80 # These don't handle the case where commits can't be compared (git branches) | |
| 81 younger_commit = make_compare(is_decendant_of) | |
| 82 passing_dicts = map(operator.itemgetter('passing_revisions'), alerts) | |
| 83 last_passing = reduce(_make_merge_dicts(younger_commit), passing_dicts) | |
| 84 | |
| 85 older_commit = make_compare(is_ancestor_of) | |
| 86 failing_dicts = map(operator.itemgetter('failing_revisions'), alerts) | |
| 87 first_failing = reduce(_make_merge_dicts(older_commit), failing_dicts) | |
| 88 | |
| 89 return last_passing, first_failing | |
| 90 | |
| 91 | |
| 92 def reason_key_for_alert(alert): | |
| 93 # FIXME: May need something smarter for reason_key. | |
| 94 reason_key = alert['step_name'] | |
| 95 if alert['reason']: | |
| 96 reason_key += ':%s' % alert['reason'] | |
| 97 else: | |
| 98 # If we don't understand the alert, just make it builder-unique. | |
| 99 reason_key += ':%s' % alert['builder_name'] | |
| 100 return reason_key | |
| 101 | |
| 102 | |
| 103 def group_by_reason(alerts): | |
| 104 by_reason = collections.defaultdict(list) | |
| 105 for alert in alerts: | |
| 106 by_reason[reason_key_for_alert(alert)].append(alert) | |
| 107 | |
| 108 reason_groups = [] | |
| 109 for reason_key, alerts in by_reason.items(): | |
| 110 last_passing, first_failing = merge_regression_ranges(alerts) | |
| 111 blame_list = flatten_to_commit_list(last_passing, first_failing) | |
| 112 # FIXME: blame_list isn't filtered yet, but should be. | |
| 113 reason_groups.append({ | |
| 114 'sort_key': reason_key, | |
| 115 'merged_last_passing': last_passing, | |
| 116 'merged_first_failing': first_failing, | |
| 117 'likely_revisions': blame_list, | |
| 118 'failure_keys': map(operator.itemgetter('key'), alerts), | |
| 119 }) | |
| 120 return reason_groups | |
| 121 | |
| 122 # http://stackoverflow.com/questions/18715688/find-common-substring-between-two- strings | |
| 123 def longestSubstringFinder(string1, string2): | |
|
ojan
2014/07/22 02:01:23
s/camelCase/camel_case
| |
| 124 answer = "" | |
| 125 len1, len2 = len(string1), len(string2) | |
| 126 for i in range(len1): | |
| 127 match = "" | |
| 128 for j in range(len2): | |
| 129 if (i + j < len1 and string1[i + j] == string2[j]): | |
| 130 match += string2[j] | |
| 131 else: | |
| 132 if (len(match) > len(answer)): answer = match | |
| 133 match = "" | |
| 134 return answer | |
| 135 | |
| 136 | |
| 137 def range_key_for_group(group): | |
| 138 last_passing = group['merged_last_passing'] | |
| 139 first_failing = group['merged_first_failing'] | |
| 140 if last_passing: | |
| 141 range_key = ' '.join(flatten_to_commit_list(last_passing, first_failing) ) | |
| 142 else: | |
| 143 # Even regressions where we don't know when they started can be | |
| 144 # merged by our earliest known failure. | |
| 145 parts = ['<=%s:%s' % (name, commit) for name, commit in first_failing.it ems()] | |
| 146 range_key = ' '.join(parts) | |
| 147 # sort_key is a heuristic to avoid merging failiures like | |
| 148 # gclient revert + webkit_tests which just happened to pull | |
| 149 # exact matching revisions when failing. | |
|
ojan
2014/07/22 02:01:23
I'm not sure it's wrong to group these together in
| |
| 150 return group['sort_key'][:3] + range_key | |
| 151 | |
| 152 | |
| 153 def merge_by_range(reason_groups): | |
| 154 if not reason_groups: | |
| 155 return [] | |
| 156 expected_keys = sorted(reason_groups[0].keys()) | |
| 157 by_range = {} | |
| 158 for group in reason_groups: | |
| 159 range_key = range_key_for_group(group) | |
| 160 existing = by_range.get(range_key) | |
| 161 if not existing: | |
| 162 # Shallow copy of group. | |
| 163 by_range[range_key] = dict(group) | |
| 164 continue | |
| 165 | |
| 166 # FIXME: It's possible we don't want to merge two keys with nothing in c ommon. | |
| 167 # e.g. bot_update and | |
| 168 # We only care about these two keys, the rest should be the same between all groups. | |
| 169 # I guess we could assert that... | |
| 170 by_range[range_key].update({ | |
| 171 'sort_key': longestSubstringFinder(existing['sort_key'], group['sort _key']), | |
| 172 'failure_keys': sorted(set(existing['failure_keys'] + group['failure _keys'])), | |
| 173 }) | |
| 174 | |
| 175 return sorted(by_range.values(), key=operator.itemgetter('sort_key')) | |
| OLD | NEW |