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 |