Index: tools/compare_codereview.py |
diff --git a/tools/compare_codereview.py b/tools/compare_codereview.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..3abf93841ae1b5d40a57211250cedef093f7e041 |
--- /dev/null |
+++ b/tools/compare_codereview.py |
@@ -0,0 +1,254 @@ |
+#!/usr/bin/python2 |
+ |
+# Copyright 2014 Google Inc. |
+# |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+ |
+"""Skia's Chromium Codereview Comparison Script. |
+ |
+This script takes two Codereview URLs, looks at the trybot results for |
+the two codereviews and compares the results. |
+ |
+Usage: |
+ compare_codereview.py CONTROL_URL ROLL_URL |
+""" |
+ |
+import collections |
+import os |
+import re |
+import sys |
+import urllib2 |
+import HTMLParser |
+ |
+ |
+class CodeReviewHTMLParser(HTMLParser.HTMLParser): |
+ """parses CodeReview web pages. |
+ """ |
+ # pylint: disable=I0011,R0904 |
+ @staticmethod |
+ def parse(url): |
+ """Returns a dictionary of {bot_name:CodeReviewHTMLParser.Status} |
+ """ |
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.
|
+ parser = CodeReviewHTMLParser() |
+ try: |
+ parser.feed(urllib2.urlopen(url).read()) |
+ except (urllib2.URLError,): |
+ print >> sys.stderr, 'Error getting', url |
+ return None |
+ parser.close() |
+ return parser.statuses |
+ |
+ Status = collections.namedtuple('Status', ['status', 'url']) |
+ |
+ def __init__(self): |
+ HTMLParser.HTMLParser.__init__(self) |
+ self._id = None |
+ self._status = None |
+ self._href = None |
+ self._anchor_data = None |
+ # statuses is a dictionary of CodeReviewHTMLParser.Status |
+ self.statuses = {} |
+ |
+ 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
|
+ attrs = dict(attrs) |
+ if tag == 'div': |
+ id_attr = attrs.get('id','') |
+ if id_attr.startswith('tryjobdiv'): |
+ self._id = id_attr |
+ if (self._id and tag == 'a' |
+ and 'build-result' in attrs.get('class', '').split()): |
+ self._status = attrs.get('status') |
+ self._href = attrs.get('href') |
+ self._anchor_data = '' |
+ |
+ def handle_endtag(self, tag): |
+ if tag == 'a' and self._status: |
+ bot = self._anchor_data.strip() |
+ stat = CodeReviewHTMLParser.Status(status=self._status, |
+ url=self._href) |
+ if bot: |
+ self.statuses[bot] = stat |
+ self._anchor_data = None |
+ self._status = None |
+ self._href = None |
+ |
+ def handle_data(self, data): |
+ if self._anchor_data is not None: |
+ 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.
|
+ |
+ |
+class BuilderHTMLParser(HTMLParser.HTMLParser): |
+ """parses Trybot web pages. |
+ """ |
+ # pylint: disable=I0011,R0904 |
+ @staticmethod |
+ def parse(url): |
+ """Returns an array of BuilderHTMLParser.Results, each a |
+ description of failure results, along with an optional url. |
+ """ |
+ parser = BuilderHTMLParser() |
+ try: |
+ parser.feed(urllib2.urlopen(url).read()) |
+ except (urllib2.URLError,): |
+ print >> sys.stderr, 'Error getting', url |
+ return [] |
+ parser.close() |
+ return parser.failure_results |
+ |
+ Result = collections.namedtuple('Result', ['text', 'url']) |
+ |
+ def __init__(self): |
+ HTMLParser.HTMLParser.__init__(self) |
+ self.failure_results = [] |
+ self._current_failure_result = None |
+ self._divlevel = None |
+ self._li_level = 0 |
+ self._li_data = '' |
+ self._current_failure = False |
+ self._failure_results_url = '' |
+ |
+ def handle_starttag(self, tag, attrs): |
+ attrs = dict(attrs) |
+ if tag == 'li': |
+ self._li_level += 1 |
+ return |
+ if tag == 'div' and attrs.get('class') == 'failure result': |
+ if self._li_level > 0: |
+ self._current_failure = True |
+ return |
+ |
+ if tag == 'a' and self._current_failure: |
+ href = attrs.get('href') |
+ if href.endswith('/logs/stdio'): |
+ self._failure_results_url = href |
+ |
+ def handle_endtag(self, tag): |
+ if tag == 'li': |
+ self._li_level -= 1 |
+ if 0 == self._li_level: |
+ if self._current_failure: |
+ result = self._li_data.strip() |
+ first = result.split()[0] |
+ if first: |
+ result = re.sub(r'^%s(\s+%s)+' % (first, first), |
+ first, result) |
+ result = re.sub(r'unexpected flaky.*', '', result) |
+ result = re.sub(r'\bpreamble\b', '', result) |
+ result = re.sub(r'\bstdio\b', '', result) |
+ url = self._failure_results_url |
+ self.failure_results.append( |
+ BuilderHTMLParser.Result(result, url)) |
+ self._current_failure_result = None |
+ self._current_failure = False |
+ self._li_data = '' |
+ self._failure_results_url = '' |
+ |
+ def handle_data(self, data): |
+ if self._current_failure: |
+ self._li_data += data |
+ |
+ |
+def printer(indent, string): |
+ """Print indented, wrapped text. |
+ """ |
+ def wrap_to(line, columns): |
+ """Wrap a line to the given number of columns, return a list |
+ of strings. |
+ """ |
+ ret = [] |
+ nextline = '' |
+ for word in line.split(): |
+ if nextline: |
+ if len(nextline) + 1 + len(word) > columns: |
+ ret.append(nextline) |
+ nextline = word |
+ else: |
+ nextline += (' ' + word) |
+ else: |
+ nextline = word |
+ if nextline: |
+ ret.append(nextline) |
+ return ret |
+ out = sys.stdout |
+ spacer = ' ' |
+ for line in string.split('\n'): |
+ for i, wrapped_line in enumerate(wrap_to(line, 68 - (2 * indent))): |
+ out.write(spacer * indent) |
+ if i > 0: |
+ out.write(spacer) |
+ out.write(wrapped_line) |
+ out.write('\n') |
+ out.flush() |
+ |
+ |
+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.
|
+ """Compare two Codereview URLs |
+ |
+ Args: |
+ control_url, roll_url: (strings) URL of the format |
+ https://codereview.chromium.org/????????? |
+ |
+ verbosity: (int) verbose level. 0, 1, or 2. |
+ """ |
+ # pylint: disable=I0011,R0914,R0912 |
+ control = CodeReviewHTMLParser.parse(control_url) |
+ roll = CodeReviewHTMLParser.parse(roll_url) |
+ if not (control and roll): |
+ return |
+ |
+ control_name = '[control %s]' % control_url.split('/')[-1] |
+ roll_name = '[roll %s]' % roll_url.split('/')[-1] |
+ all_bots = set(control) & set(roll) |
+ |
+ if verbosity > 0: |
+ print '%11s %11s %4s %s' % ('CONTROL', 'ROLL', 'DIFF', 'BOT') |
+ for bot in sorted(all_bots): |
+ if control[bot].status != roll[bot].status: |
+ diff = '****' |
+ elif (control[bot].status != 'success' or |
+ roll[bot].status != 'success'): |
+ diff = '....' |
+ else: |
+ diff = '' |
+ print '%11s %11s %4s %s' % ( |
+ control[bot].status, roll[bot].status, diff, bot) |
+ sys.stdout.flush() |
+ |
+ for bot in sorted(all_bots): |
+ if (roll[bot].status == 'success'): |
+ if verbosity > 1: |
+ print '\n==%s==' % bot |
+ printer(1, 'OK') |
+ continue |
+ print '\n==%s==' % bot |
+ |
+ for (status, name, url) in ( |
+ (control[bot].status, control_name, control[bot].url), |
+ (roll[bot].status, roll_name, roll[bot].url)): |
+ |
+ if status == 'failure': |
+ printer(1, name) |
+ results = BuilderHTMLParser.parse(url) |
+ for result in results: |
+ formatted_result = re.sub(r'(\S*\.html) ', '\n__\g<1>\n', |
+ result.text) |
+ printer(2, formatted_result) |
+ if ('compile' in result.text |
+ or '...and more' in result.text): |
+ printer(3, re.sub('/[^/]*$', '/', url) + result.url ) |
+ else: |
+ printer(1, name) |
+ printer(2, status) |
+ |
+ |
+if __name__ == '__main__': |
+ if len(sys.argv) < 3: |
+ print >> sys.stderr, __doc__ |
+ exit(1) |
+ main(sys.argv[1], sys.argv[2], |
+ int(os.environ.get('COMPARE_CODEREVIEW_VERBOSITY', 1))) |
+ |
+ |