Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 #!/usr/bin/python2 | |
| 2 | |
| 3 # Copyright 2014 Google Inc. | |
| 4 # | |
| 5 # Use of this source code is governed by a BSD-style license that can be | |
| 6 # found in the LICENSE file. | |
| 7 | |
| 8 """Skia's Chromium Codereview Comparison Script. | |
| 9 | |
| 10 This script takes two Codereview URLs, looks at the trybot results for | |
| 11 the two codereviews and compares the results. | |
| 12 | |
| 13 Usage: | |
| 14 compare_codereview.py CONTROL_URL ROLL_URL | |
| 15 """ | |
| 16 | |
| 17 import collections | |
| 18 import os | |
| 19 import re | |
| 20 import sys | |
| 21 import urllib2 | |
| 22 import HTMLParser | |
| 23 | |
| 24 | |
| 25 class CodeReviewHTMLParser(HTMLParser.HTMLParser): | |
| 26 """parses CodeReview web pages. | |
| 27 """ | |
| 28 # pylint: disable=I0011,R0904 | |
| 29 @staticmethod | |
| 30 def parse(url): | |
| 31 """Returns a dictionary of {bot_name:CodeReviewHTMLParser.Status} | |
| 32 """ | |
|
borenet
2014/01/21 21:55:43
I'd prefer that you use the following docstring fo
hal.canary
2014/01/22 15:25:32
Done.
| |
| 33 parser = CodeReviewHTMLParser() | |
| 34 try: | |
| 35 parser.feed(urllib2.urlopen(url).read()) | |
| 36 except (urllib2.URLError,): | |
| 37 print >> sys.stderr, 'Error getting', url | |
| 38 return None | |
| 39 parser.close() | |
| 40 return parser.statuses | |
| 41 | |
| 42 Status = collections.namedtuple('Status', ['status', 'url']) | |
| 43 | |
| 44 def __init__(self): | |
| 45 HTMLParser.HTMLParser.__init__(self) | |
| 46 self._id = None | |
| 47 self._status = None | |
| 48 self._href = None | |
| 49 self._anchor_data = None | |
| 50 # statuses is a dictionary of CodeReviewHTMLParser.Status | |
| 51 self.statuses = {} | |
| 52 | |
| 53 def handle_starttag(self, tag, attrs): | |
|
borenet
2014/01/21 21:55:43
I'd appreciate a docstring here and elsewhere, eve
hal.canary
2014/01/22 15:25:32
I'll just note that I'm overriding a method and co
| |
| 54 attrs = dict(attrs) | |
| 55 if tag == 'div': | |
| 56 id_attr = attrs.get('id','') | |
| 57 if id_attr.startswith('tryjobdiv'): | |
| 58 self._id = id_attr | |
| 59 if (self._id and tag == 'a' | |
| 60 and 'build-result' in attrs.get('class', '').split()): | |
| 61 self._status = attrs.get('status') | |
| 62 self._href = attrs.get('href') | |
| 63 self._anchor_data = '' | |
| 64 | |
| 65 def handle_endtag(self, tag): | |
| 66 if tag == 'a' and self._status: | |
| 67 bot = self._anchor_data.strip() | |
| 68 stat = CodeReviewHTMLParser.Status(status=self._status, | |
| 69 url=self._href) | |
| 70 if bot: | |
| 71 self.statuses[bot] = stat | |
| 72 self._anchor_data = None | |
| 73 self._status = None | |
| 74 self._href = None | |
| 75 | |
| 76 def handle_data(self, data): | |
| 77 if self._anchor_data is not None: | |
| 78 self._anchor_data += data | |
|
borenet
2014/01/21 21:55:43
I find this flow hard to follow; I think I'd rathe
hal.canary
2014/01/22 15:25:32
Done.
| |
| 79 | |
| 80 | |
| 81 class BuilderHTMLParser(HTMLParser.HTMLParser): | |
| 82 """parses Trybot web pages. | |
| 83 """ | |
| 84 # pylint: disable=I0011,R0904 | |
| 85 @staticmethod | |
| 86 def parse(url): | |
| 87 """Returns an array of BuilderHTMLParser.Results, each a | |
| 88 description of failure results, along with an optional url. | |
| 89 """ | |
| 90 parser = BuilderHTMLParser() | |
| 91 try: | |
| 92 parser.feed(urllib2.urlopen(url).read()) | |
| 93 except (urllib2.URLError,): | |
| 94 print >> sys.stderr, 'Error getting', url | |
| 95 return [] | |
| 96 parser.close() | |
| 97 return parser.failure_results | |
| 98 | |
| 99 Result = collections.namedtuple('Result', ['text', 'url']) | |
| 100 | |
| 101 def __init__(self): | |
| 102 HTMLParser.HTMLParser.__init__(self) | |
| 103 self.failure_results = [] | |
| 104 self._current_failure_result = None | |
| 105 self._divlevel = None | |
| 106 self._li_level = 0 | |
| 107 self._li_data = '' | |
| 108 self._current_failure = False | |
| 109 self._failure_results_url = '' | |
| 110 | |
| 111 def handle_starttag(self, tag, attrs): | |
| 112 attrs = dict(attrs) | |
| 113 if tag == 'li': | |
| 114 self._li_level += 1 | |
| 115 return | |
| 116 if tag == 'div' and attrs.get('class') == 'failure result': | |
| 117 if self._li_level > 0: | |
| 118 self._current_failure = True | |
| 119 return | |
| 120 | |
| 121 if tag == 'a' and self._current_failure: | |
| 122 href = attrs.get('href') | |
| 123 if href.endswith('/logs/stdio'): | |
| 124 self._failure_results_url = href | |
| 125 | |
| 126 def handle_endtag(self, tag): | |
| 127 if tag == 'li': | |
| 128 self._li_level -= 1 | |
| 129 if 0 == self._li_level: | |
| 130 if self._current_failure: | |
| 131 result = self._li_data.strip() | |
| 132 first = result.split()[0] | |
| 133 if first: | |
| 134 result = re.sub(r'^%s(\s+%s)+' % (first, first), | |
| 135 first, result) | |
| 136 result = re.sub(r'unexpected flaky.*', '', result) | |
| 137 result = re.sub(r'\bpreamble\b', '', result) | |
| 138 result = re.sub(r'\bstdio\b', '', result) | |
| 139 url = self._failure_results_url | |
| 140 self.failure_results.append( | |
| 141 BuilderHTMLParser.Result(result, url)) | |
| 142 self._current_failure_result = None | |
| 143 self._current_failure = False | |
| 144 self._li_data = '' | |
| 145 self._failure_results_url = '' | |
| 146 | |
| 147 def handle_data(self, data): | |
| 148 if self._current_failure: | |
| 149 self._li_data += data | |
| 150 | |
| 151 | |
| 152 def printer(indent, string): | |
| 153 """Print indented, wrapped text. | |
| 154 """ | |
| 155 def wrap_to(line, columns): | |
| 156 """Wrap a line to the given number of columns, return a list | |
| 157 of strings. | |
| 158 """ | |
| 159 ret = [] | |
| 160 nextline = '' | |
| 161 for word in line.split(): | |
| 162 if nextline: | |
| 163 if len(nextline) + 1 + len(word) > columns: | |
| 164 ret.append(nextline) | |
| 165 nextline = word | |
| 166 else: | |
| 167 nextline += (' ' + word) | |
| 168 else: | |
| 169 nextline = word | |
| 170 if nextline: | |
| 171 ret.append(nextline) | |
| 172 return ret | |
| 173 out = sys.stdout | |
| 174 spacer = ' ' | |
| 175 for line in string.split('\n'): | |
| 176 for i, wrapped_line in enumerate(wrap_to(line, 68 - (2 * indent))): | |
| 177 out.write(spacer * indent) | |
| 178 if i > 0: | |
| 179 out.write(spacer) | |
| 180 out.write(wrapped_line) | |
| 181 out.write('\n') | |
| 182 out.flush() | |
| 183 | |
| 184 | |
| 185 def main(control_url, roll_url, verbosity): | |
|
borenet
2014/01/21 21:55:43
Optional: you could add a default verbosity level
hal.canary
2014/01/22 15:25:32
Done.
| |
| 186 """Compare two Codereview URLs | |
| 187 | |
| 188 Args: | |
| 189 control_url, roll_url: (strings) URL of the format | |
| 190 https://codereview.chromium.org/????????? | |
| 191 | |
| 192 verbosity: (int) verbose level. 0, 1, or 2. | |
| 193 """ | |
| 194 # pylint: disable=I0011,R0914,R0912 | |
| 195 control = CodeReviewHTMLParser.parse(control_url) | |
| 196 roll = CodeReviewHTMLParser.parse(roll_url) | |
| 197 if not (control and roll): | |
| 198 return | |
| 199 | |
| 200 control_name = '[control %s]' % control_url.split('/')[-1] | |
| 201 roll_name = '[roll %s]' % roll_url.split('/')[-1] | |
| 202 all_bots = set(control) & set(roll) | |
| 203 | |
| 204 if verbosity > 0: | |
| 205 print '%11s %11s %4s %s' % ('CONTROL', 'ROLL', 'DIFF', 'BOT') | |
| 206 print | |
| 207 for bot in sorted(all_bots): | |
| 208 if control[bot].status != roll[bot].status: | |
| 209 diff = '****' | |
| 210 elif (control[bot].status != 'success' or | |
| 211 roll[bot].status != 'success'): | |
| 212 diff = '....' | |
| 213 else: | |
| 214 diff = '' | |
| 215 print '%11s %11s %4s %s' % ( | |
| 216 control[bot].status, roll[bot].status, diff, bot) | |
| 217 print | |
| 218 sys.stdout.flush() | |
| 219 | |
| 220 for bot in sorted(all_bots): | |
| 221 if (roll[bot].status == 'success'): | |
| 222 if verbosity > 1: | |
| 223 print '\n==%s==' % bot | |
| 224 printer(1, 'OK') | |
| 225 continue | |
| 226 print '\n==%s==' % bot | |
| 227 | |
| 228 for (status, name, url) in ( | |
| 229 (control[bot].status, control_name, control[bot].url), | |
| 230 (roll[bot].status, roll_name, roll[bot].url)): | |
| 231 | |
| 232 if status == 'failure': | |
| 233 printer(1, name) | |
| 234 results = BuilderHTMLParser.parse(url) | |
| 235 for result in results: | |
| 236 formatted_result = re.sub(r'(\S*\.html) ', '\n__\g<1>\n', | |
| 237 result.text) | |
| 238 printer(2, formatted_result) | |
| 239 if ('compile' in result.text | |
| 240 or '...and more' in result.text): | |
| 241 printer(3, re.sub('/[^/]*$', '/', url) + result.url ) | |
| 242 else: | |
| 243 printer(1, name) | |
| 244 printer(2, status) | |
| 245 | |
| 246 | |
| 247 if __name__ == '__main__': | |
| 248 if len(sys.argv) < 3: | |
| 249 print >> sys.stderr, __doc__ | |
| 250 exit(1) | |
| 251 main(sys.argv[1], sys.argv[2], | |
| 252 int(os.environ.get('COMPARE_CODEREVIEW_VERBOSITY', 1))) | |
| 253 | |
| 254 | |
| OLD | NEW |