1cb93a386Sopenharmony_ci#!/usr/bin/python2 2cb93a386Sopenharmony_ci 3cb93a386Sopenharmony_ci# Copyright 2014 Google Inc. 4cb93a386Sopenharmony_ci# 5cb93a386Sopenharmony_ci# Use of this source code is governed by a BSD-style license that can be 6cb93a386Sopenharmony_ci# found in the LICENSE file. 7cb93a386Sopenharmony_ci 8cb93a386Sopenharmony_ci"""Skia's Chromium Codereview Comparison Script. 9cb93a386Sopenharmony_ci 10cb93a386Sopenharmony_ciThis script takes two Codereview URLs, looks at the trybot results for 11cb93a386Sopenharmony_cithe two codereviews and compares the results. 12cb93a386Sopenharmony_ci 13cb93a386Sopenharmony_ciUsage: 14cb93a386Sopenharmony_ci compare_codereview.py CONTROL_URL ROLL_URL 15cb93a386Sopenharmony_ci""" 16cb93a386Sopenharmony_ci 17cb93a386Sopenharmony_ciimport collections 18cb93a386Sopenharmony_ciimport os 19cb93a386Sopenharmony_ciimport re 20cb93a386Sopenharmony_ciimport sys 21cb93a386Sopenharmony_ciimport urllib2 22cb93a386Sopenharmony_ciimport HTMLParser 23cb93a386Sopenharmony_ci 24cb93a386Sopenharmony_ci 25cb93a386Sopenharmony_ciclass CodeReviewHTMLParser(HTMLParser.HTMLParser): 26cb93a386Sopenharmony_ci """Parses CodeReview web page. 27cb93a386Sopenharmony_ci 28cb93a386Sopenharmony_ci Use the CodeReviewHTMLParser.parse static function to make use of 29cb93a386Sopenharmony_ci this class. 30cb93a386Sopenharmony_ci 31cb93a386Sopenharmony_ci This uses the HTMLParser class because it's the best thing in 32cb93a386Sopenharmony_ci Python's standard library. We need a little more power than a 33cb93a386Sopenharmony_ci regex. [Search for "You can't parse [X]HTML with regex." for more 34cb93a386Sopenharmony_ci information. 35cb93a386Sopenharmony_ci """ 36cb93a386Sopenharmony_ci # pylint: disable=I0011,R0904 37cb93a386Sopenharmony_ci @staticmethod 38cb93a386Sopenharmony_ci def parse(url): 39cb93a386Sopenharmony_ci """Parses a CodeReview web pages. 40cb93a386Sopenharmony_ci 41cb93a386Sopenharmony_ci Args: 42cb93a386Sopenharmony_ci url (string), a codereview URL like this: 43cb93a386Sopenharmony_ci 'https://codereview.chromium.org/?????????'. 44cb93a386Sopenharmony_ci 45cb93a386Sopenharmony_ci Returns: 46cb93a386Sopenharmony_ci A dictionary; the keys are bot_name strings, the values 47cb93a386Sopenharmony_ci are CodeReviewHTMLParser.Status objects 48cb93a386Sopenharmony_ci """ 49cb93a386Sopenharmony_ci parser = CodeReviewHTMLParser() 50cb93a386Sopenharmony_ci try: 51cb93a386Sopenharmony_ci parser.feed(urllib2.urlopen(url).read()) 52cb93a386Sopenharmony_ci except (urllib2.URLError,): 53cb93a386Sopenharmony_ci print >> sys.stderr, 'Error getting', url 54cb93a386Sopenharmony_ci return None 55cb93a386Sopenharmony_ci parser.close() 56cb93a386Sopenharmony_ci return parser.statuses 57cb93a386Sopenharmony_ci 58cb93a386Sopenharmony_ci # namedtuples are like lightweight structs in Python. The low 59cb93a386Sopenharmony_ci # overhead of a tuple, but the ease of use of an object. 60cb93a386Sopenharmony_ci Status = collections.namedtuple('Status', ['status', 'url']) 61cb93a386Sopenharmony_ci 62cb93a386Sopenharmony_ci def __init__(self): 63cb93a386Sopenharmony_ci HTMLParser.HTMLParser.__init__(self) 64cb93a386Sopenharmony_ci self._id = None 65cb93a386Sopenharmony_ci self._status = None 66cb93a386Sopenharmony_ci self._href = None 67cb93a386Sopenharmony_ci self._anchor_data = '' 68cb93a386Sopenharmony_ci self._currently_parsing_trybotdiv = False 69cb93a386Sopenharmony_ci # statuses is a dictionary of CodeReviewHTMLParser.Status 70cb93a386Sopenharmony_ci self.statuses = {} 71cb93a386Sopenharmony_ci 72cb93a386Sopenharmony_ci def handle_starttag(self, tag, attrs): 73cb93a386Sopenharmony_ci """Overrides the HTMLParser method to implement functionality. 74cb93a386Sopenharmony_ci 75cb93a386Sopenharmony_ci [[begin standard library documentation]] 76cb93a386Sopenharmony_ci This method is called to handle the start of a tag 77cb93a386Sopenharmony_ci (e.g. <div id="main">). 78cb93a386Sopenharmony_ci 79cb93a386Sopenharmony_ci The tag argument is the name of the tag converted to lower 80cb93a386Sopenharmony_ci case. The attrs argument is a list of (name, value) pairs 81cb93a386Sopenharmony_ci containing the attributes found inside the tag's <> 82cb93a386Sopenharmony_ci brackets. The name will be translated to lower case, and 83cb93a386Sopenharmony_ci quotes in the value have been removed, and character and 84cb93a386Sopenharmony_ci entity references have been replaced. 85cb93a386Sopenharmony_ci 86cb93a386Sopenharmony_ci For instance, for the tag <A HREF="http://www.cwi.nl/">, this 87cb93a386Sopenharmony_ci method would be called as handle_starttag('a', [('href', 88cb93a386Sopenharmony_ci 'http://www.cwi.nl/')]). 89cb93a386Sopenharmony_ci [[end standard library documentation]] 90cb93a386Sopenharmony_ci """ 91cb93a386Sopenharmony_ci attrs = dict(attrs) 92cb93a386Sopenharmony_ci if tag == 'div': 93cb93a386Sopenharmony_ci # We are looking for <div id="tryjobdiv*">. 94cb93a386Sopenharmony_ci id_attr = attrs.get('id','') 95cb93a386Sopenharmony_ci if id_attr.startswith('tryjobdiv'): 96cb93a386Sopenharmony_ci self._id = id_attr 97cb93a386Sopenharmony_ci if (self._id and tag == 'a' 98cb93a386Sopenharmony_ci and 'build-result' in attrs.get('class', '').split()): 99cb93a386Sopenharmony_ci # If we are already inside a <div id="tryjobdiv*">, we 100cb93a386Sopenharmony_ci # look for a link if the form 101cb93a386Sopenharmony_ci # <a class="build-result" href="*">. Then we save the 102cb93a386Sopenharmony_ci # (non-standard) status attribute and the URL. 103cb93a386Sopenharmony_ci self._status = attrs.get('status') 104cb93a386Sopenharmony_ci self._href = attrs.get('href') 105cb93a386Sopenharmony_ci self._currently_parsing_trybotdiv = True 106cb93a386Sopenharmony_ci # Start saving anchor data. 107cb93a386Sopenharmony_ci 108cb93a386Sopenharmony_ci def handle_data(self, data): 109cb93a386Sopenharmony_ci """Overrides the HTMLParser method to implement functionality. 110cb93a386Sopenharmony_ci 111cb93a386Sopenharmony_ci [[begin standard library documentation]] 112cb93a386Sopenharmony_ci This method is called to process arbitrary data (e.g. text 113cb93a386Sopenharmony_ci nodes and the content of <script>...</script> and 114cb93a386Sopenharmony_ci <style>...</style>). 115cb93a386Sopenharmony_ci [[end standard library documentation]] 116cb93a386Sopenharmony_ci """ 117cb93a386Sopenharmony_ci # Save the text inside the <a></a> tags. Assume <a> tags 118cb93a386Sopenharmony_ci # aren't nested. 119cb93a386Sopenharmony_ci if self._currently_parsing_trybotdiv: 120cb93a386Sopenharmony_ci self._anchor_data += data 121cb93a386Sopenharmony_ci 122cb93a386Sopenharmony_ci def handle_endtag(self, tag): 123cb93a386Sopenharmony_ci """Overrides the HTMLParser method to implement functionality. 124cb93a386Sopenharmony_ci 125cb93a386Sopenharmony_ci [[begin standard library documentation]] 126cb93a386Sopenharmony_ci This method is called to handle the end tag of an element 127cb93a386Sopenharmony_ci (e.g. </div>). The tag argument is the name of the tag 128cb93a386Sopenharmony_ci converted to lower case. 129cb93a386Sopenharmony_ci [[end standard library documentation]] 130cb93a386Sopenharmony_ci """ 131cb93a386Sopenharmony_ci if tag == 'a' and self._status: 132cb93a386Sopenharmony_ci # We take the accumulated self._anchor_data and save it as 133cb93a386Sopenharmony_ci # the bot name. 134cb93a386Sopenharmony_ci bot = self._anchor_data.strip() 135cb93a386Sopenharmony_ci stat = CodeReviewHTMLParser.Status(status=self._status, 136cb93a386Sopenharmony_ci url=self._href) 137cb93a386Sopenharmony_ci if bot: 138cb93a386Sopenharmony_ci # Add to accumulating dictionary. 139cb93a386Sopenharmony_ci self.statuses[bot] = stat 140cb93a386Sopenharmony_ci # Reset state to search for the next bot. 141cb93a386Sopenharmony_ci self._currently_parsing_trybotdiv = False 142cb93a386Sopenharmony_ci self._anchor_data = '' 143cb93a386Sopenharmony_ci self._status = None 144cb93a386Sopenharmony_ci self._href = None 145cb93a386Sopenharmony_ci 146cb93a386Sopenharmony_ci 147cb93a386Sopenharmony_ciclass BuilderHTMLParser(HTMLParser.HTMLParser): 148cb93a386Sopenharmony_ci """parses Trybot web pages. 149cb93a386Sopenharmony_ci 150cb93a386Sopenharmony_ci Use the BuilderHTMLParser.parse static function to make use of 151cb93a386Sopenharmony_ci this class. 152cb93a386Sopenharmony_ci 153cb93a386Sopenharmony_ci This uses the HTMLParser class because it's the best thing in 154cb93a386Sopenharmony_ci Python's standard library. We need a little more power than a 155cb93a386Sopenharmony_ci regex. [Search for "You can't parse [X]HTML with regex." for more 156cb93a386Sopenharmony_ci information. 157cb93a386Sopenharmony_ci """ 158cb93a386Sopenharmony_ci # pylint: disable=I0011,R0904 159cb93a386Sopenharmony_ci @staticmethod 160cb93a386Sopenharmony_ci def parse(url): 161cb93a386Sopenharmony_ci """Parses a Trybot web page. 162cb93a386Sopenharmony_ci 163cb93a386Sopenharmony_ci Args: 164cb93a386Sopenharmony_ci url (string), a trybot result URL. 165cb93a386Sopenharmony_ci 166cb93a386Sopenharmony_ci Returns: 167cb93a386Sopenharmony_ci An array of BuilderHTMLParser.Results, each a description 168cb93a386Sopenharmony_ci of failure results, along with an optional url 169cb93a386Sopenharmony_ci """ 170cb93a386Sopenharmony_ci parser = BuilderHTMLParser() 171cb93a386Sopenharmony_ci try: 172cb93a386Sopenharmony_ci parser.feed(urllib2.urlopen(url).read()) 173cb93a386Sopenharmony_ci except (urllib2.URLError,): 174cb93a386Sopenharmony_ci print >> sys.stderr, 'Error getting', url 175cb93a386Sopenharmony_ci return [] 176cb93a386Sopenharmony_ci parser.close() 177cb93a386Sopenharmony_ci return parser.failure_results 178cb93a386Sopenharmony_ci 179cb93a386Sopenharmony_ci Result = collections.namedtuple('Result', ['text', 'url']) 180cb93a386Sopenharmony_ci 181cb93a386Sopenharmony_ci def __init__(self): 182cb93a386Sopenharmony_ci HTMLParser.HTMLParser.__init__(self) 183cb93a386Sopenharmony_ci self.failure_results = [] 184cb93a386Sopenharmony_ci self._current_failure_result = None 185cb93a386Sopenharmony_ci self._divlevel = None 186cb93a386Sopenharmony_ci self._li_level = 0 187cb93a386Sopenharmony_ci self._li_data = '' 188cb93a386Sopenharmony_ci self._current_failure = False 189cb93a386Sopenharmony_ci self._failure_results_url = '' 190cb93a386Sopenharmony_ci 191cb93a386Sopenharmony_ci def handle_starttag(self, tag, attrs): 192cb93a386Sopenharmony_ci """Overrides the HTMLParser method to implement functionality. 193cb93a386Sopenharmony_ci 194cb93a386Sopenharmony_ci [[begin standard library documentation]] 195cb93a386Sopenharmony_ci This method is called to handle the start of a tag 196cb93a386Sopenharmony_ci (e.g. <div id="main">). 197cb93a386Sopenharmony_ci 198cb93a386Sopenharmony_ci The tag argument is the name of the tag converted to lower 199cb93a386Sopenharmony_ci case. The attrs argument is a list of (name, value) pairs 200cb93a386Sopenharmony_ci containing the attributes found inside the tag's <> 201cb93a386Sopenharmony_ci brackets. The name will be translated to lower case, and 202cb93a386Sopenharmony_ci quotes in the value have been removed, and character and 203cb93a386Sopenharmony_ci entity references have been replaced. 204cb93a386Sopenharmony_ci 205cb93a386Sopenharmony_ci For instance, for the tag <A HREF="http://www.cwi.nl/">, this 206cb93a386Sopenharmony_ci method would be called as handle_starttag('a', [('href', 207cb93a386Sopenharmony_ci 'http://www.cwi.nl/')]). 208cb93a386Sopenharmony_ci [[end standard library documentation]] 209cb93a386Sopenharmony_ci """ 210cb93a386Sopenharmony_ci attrs = dict(attrs) 211cb93a386Sopenharmony_ci if tag == 'li': 212cb93a386Sopenharmony_ci # <li> tags can be nested. So we have to count the 213cb93a386Sopenharmony_ci # nest-level for backing out. 214cb93a386Sopenharmony_ci self._li_level += 1 215cb93a386Sopenharmony_ci return 216cb93a386Sopenharmony_ci if tag == 'div' and attrs.get('class') == 'failure result': 217cb93a386Sopenharmony_ci # We care about this sort of thing: 218cb93a386Sopenharmony_ci # <li> 219cb93a386Sopenharmony_ci # <li> 220cb93a386Sopenharmony_ci # <li> 221cb93a386Sopenharmony_ci # <div class="failure result">...</div> 222cb93a386Sopenharmony_ci # </li> 223cb93a386Sopenharmony_ci # </li> 224cb93a386Sopenharmony_ci # We want this text here. 225cb93a386Sopenharmony_ci # </li> 226cb93a386Sopenharmony_ci if self._li_level > 0: 227cb93a386Sopenharmony_ci self._current_failure = True # Tells us to keep text. 228cb93a386Sopenharmony_ci return 229cb93a386Sopenharmony_ci 230cb93a386Sopenharmony_ci if tag == 'a' and self._current_failure: 231cb93a386Sopenharmony_ci href = attrs.get('href') 232cb93a386Sopenharmony_ci # Sometimes we want to keep the stdio url. We always 233cb93a386Sopenharmony_ci # return it, just in case. 234cb93a386Sopenharmony_ci if href.endswith('/logs/stdio'): 235cb93a386Sopenharmony_ci self._failure_results_url = href 236cb93a386Sopenharmony_ci 237cb93a386Sopenharmony_ci def handle_data(self, data): 238cb93a386Sopenharmony_ci """Overrides the HTMLParser method to implement functionality. 239cb93a386Sopenharmony_ci 240cb93a386Sopenharmony_ci [[begin standard library documentation]] 241cb93a386Sopenharmony_ci This method is called to process arbitrary data (e.g. text 242cb93a386Sopenharmony_ci nodes and the content of <script>...</script> and 243cb93a386Sopenharmony_ci <style>...</style>). 244cb93a386Sopenharmony_ci [[end standard library documentation]] 245cb93a386Sopenharmony_ci """ 246cb93a386Sopenharmony_ci if self._current_failure: 247cb93a386Sopenharmony_ci self._li_data += data 248cb93a386Sopenharmony_ci 249cb93a386Sopenharmony_ci def handle_endtag(self, tag): 250cb93a386Sopenharmony_ci """Overrides the HTMLParser method to implement functionality. 251cb93a386Sopenharmony_ci 252cb93a386Sopenharmony_ci [[begin standard library documentation]] 253cb93a386Sopenharmony_ci This method is called to handle the end tag of an element 254cb93a386Sopenharmony_ci (e.g. </div>). The tag argument is the name of the tag 255cb93a386Sopenharmony_ci converted to lower case. 256cb93a386Sopenharmony_ci [[end standard library documentation]] 257cb93a386Sopenharmony_ci """ 258cb93a386Sopenharmony_ci if tag == 'li': 259cb93a386Sopenharmony_ci self._li_level -= 1 260cb93a386Sopenharmony_ci if 0 == self._li_level: 261cb93a386Sopenharmony_ci if self._current_failure: 262cb93a386Sopenharmony_ci result = self._li_data.strip() 263cb93a386Sopenharmony_ci first = result.split()[0] 264cb93a386Sopenharmony_ci if first: 265cb93a386Sopenharmony_ci result = re.sub( 266cb93a386Sopenharmony_ci r'^%s(\s+%s)+' % (first, first), first, result) 267cb93a386Sopenharmony_ci # Sometimes, it repeats the same thing 268cb93a386Sopenharmony_ci # multiple times. 269cb93a386Sopenharmony_ci result = re.sub(r'unexpected flaky.*', '', result) 270cb93a386Sopenharmony_ci # Remove some extra unnecessary text. 271cb93a386Sopenharmony_ci result = re.sub(r'\bpreamble\b', '', result) 272cb93a386Sopenharmony_ci result = re.sub(r'\bstdio\b', '', result) 273cb93a386Sopenharmony_ci url = self._failure_results_url 274cb93a386Sopenharmony_ci self.failure_results.append( 275cb93a386Sopenharmony_ci BuilderHTMLParser.Result(result, url)) 276cb93a386Sopenharmony_ci self._current_failure_result = None 277cb93a386Sopenharmony_ci # Reset the state. 278cb93a386Sopenharmony_ci self._current_failure = False 279cb93a386Sopenharmony_ci self._li_data = '' 280cb93a386Sopenharmony_ci self._failure_results_url = '' 281cb93a386Sopenharmony_ci 282cb93a386Sopenharmony_ci 283cb93a386Sopenharmony_cidef printer(indent, string): 284cb93a386Sopenharmony_ci """Print indented, wrapped text. 285cb93a386Sopenharmony_ci """ 286cb93a386Sopenharmony_ci def wrap_to(line, columns): 287cb93a386Sopenharmony_ci """Wrap a line to the given number of columns, return a list 288cb93a386Sopenharmony_ci of strings. 289cb93a386Sopenharmony_ci """ 290cb93a386Sopenharmony_ci ret = [] 291cb93a386Sopenharmony_ci nextline = '' 292cb93a386Sopenharmony_ci for word in line.split(): 293cb93a386Sopenharmony_ci if nextline: 294cb93a386Sopenharmony_ci if len(nextline) + 1 + len(word) > columns: 295cb93a386Sopenharmony_ci ret.append(nextline) 296cb93a386Sopenharmony_ci nextline = word 297cb93a386Sopenharmony_ci else: 298cb93a386Sopenharmony_ci nextline += (' ' + word) 299cb93a386Sopenharmony_ci else: 300cb93a386Sopenharmony_ci nextline = word 301cb93a386Sopenharmony_ci if nextline: 302cb93a386Sopenharmony_ci ret.append(nextline) 303cb93a386Sopenharmony_ci return ret 304cb93a386Sopenharmony_ci out = sys.stdout 305cb93a386Sopenharmony_ci spacer = ' ' 306cb93a386Sopenharmony_ci for line in string.split('\n'): 307cb93a386Sopenharmony_ci for i, wrapped_line in enumerate(wrap_to(line, 68 - (2 * indent))): 308cb93a386Sopenharmony_ci out.write(spacer * indent) 309cb93a386Sopenharmony_ci if i > 0: 310cb93a386Sopenharmony_ci out.write(spacer) 311cb93a386Sopenharmony_ci out.write(wrapped_line) 312cb93a386Sopenharmony_ci out.write('\n') 313cb93a386Sopenharmony_ci out.flush() 314cb93a386Sopenharmony_ci 315cb93a386Sopenharmony_ci 316cb93a386Sopenharmony_cidef main(control_url, roll_url, verbosity=1): 317cb93a386Sopenharmony_ci """Compare two Codereview URLs 318cb93a386Sopenharmony_ci 319cb93a386Sopenharmony_ci Args: 320cb93a386Sopenharmony_ci control_url, roll_url: (strings) URL of the format 321cb93a386Sopenharmony_ci https://codereview.chromium.org/????????? 322cb93a386Sopenharmony_ci 323cb93a386Sopenharmony_ci verbosity: (int) verbose level. 0, 1, or 2. 324cb93a386Sopenharmony_ci """ 325cb93a386Sopenharmony_ci # pylint: disable=I0011,R0914,R0912 326cb93a386Sopenharmony_ci control = CodeReviewHTMLParser.parse(control_url) 327cb93a386Sopenharmony_ci roll = CodeReviewHTMLParser.parse(roll_url) 328cb93a386Sopenharmony_ci all_bots = set(control) & set(roll) # Set intersection. 329cb93a386Sopenharmony_ci if not all_bots: 330cb93a386Sopenharmony_ci print >> sys.stderr, ( 331cb93a386Sopenharmony_ci 'Error: control %s and roll %s have no common trybots.' 332cb93a386Sopenharmony_ci % (list(control), list(roll))) 333cb93a386Sopenharmony_ci return 334cb93a386Sopenharmony_ci 335cb93a386Sopenharmony_ci control_name = '[control %s]' % control_url.split('/')[-1] 336cb93a386Sopenharmony_ci roll_name = '[roll %s]' % roll_url.split('/')[-1] 337cb93a386Sopenharmony_ci 338cb93a386Sopenharmony_ci out = sys.stdout 339cb93a386Sopenharmony_ci 340cb93a386Sopenharmony_ci for bot in sorted(all_bots): 341cb93a386Sopenharmony_ci if (roll[bot].status == 'success'): 342cb93a386Sopenharmony_ci if verbosity > 1: 343cb93a386Sopenharmony_ci printer(0, '==%s==' % bot) 344cb93a386Sopenharmony_ci printer(1, 'OK') 345cb93a386Sopenharmony_ci continue 346cb93a386Sopenharmony_ci 347cb93a386Sopenharmony_ci if control[bot].status != 'failure' and roll[bot].status != 'failure': 348cb93a386Sopenharmony_ci continue 349cb93a386Sopenharmony_ci printer(0, '==%s==' % bot) 350cb93a386Sopenharmony_ci 351cb93a386Sopenharmony_ci formatted_results = [] 352cb93a386Sopenharmony_ci for (status, name, url) in [ 353cb93a386Sopenharmony_ci (control[bot].status, control_name, control[bot].url), 354cb93a386Sopenharmony_ci ( roll[bot].status, roll_name, roll[bot].url)]: 355cb93a386Sopenharmony_ci lines = [] 356cb93a386Sopenharmony_ci if status == 'failure': 357cb93a386Sopenharmony_ci results = BuilderHTMLParser.parse(url) 358cb93a386Sopenharmony_ci for result in results: 359cb93a386Sopenharmony_ci formatted_result = re.sub(r'(\S*\.html) ', '\n__\g<1>\n', result.text) 360cb93a386Sopenharmony_ci # Strip runtimes. 361cb93a386Sopenharmony_ci formatted_result = re.sub(r'\(.*\)', '', formatted_result) 362cb93a386Sopenharmony_ci lines.append((2, formatted_result)) 363cb93a386Sopenharmony_ci if ('compile' in result.text or '...and more' in result.text): 364cb93a386Sopenharmony_ci lines.append((3, re.sub('/[^/]*$', '/', url) + result.url)) 365cb93a386Sopenharmony_ci formatted_results.append(lines) 366cb93a386Sopenharmony_ci 367cb93a386Sopenharmony_ci identical = formatted_results[0] == formatted_results[1] 368cb93a386Sopenharmony_ci 369cb93a386Sopenharmony_ci 370cb93a386Sopenharmony_ci for (formatted_result, (status, name, url)) in zip( 371cb93a386Sopenharmony_ci formatted_results, 372cb93a386Sopenharmony_ci [(control[bot].status, control_name, control[bot].url), 373cb93a386Sopenharmony_ci (roll[bot].status, roll_name, roll[bot].url)]): 374cb93a386Sopenharmony_ci if status != 'failure' and not identical: 375cb93a386Sopenharmony_ci printer(1, name) 376cb93a386Sopenharmony_ci printer(2, status) 377cb93a386Sopenharmony_ci elif status == 'failure': 378cb93a386Sopenharmony_ci if identical: 379cb93a386Sopenharmony_ci printer(1, control_name + ' and ' + roll_name + ' failed identically') 380cb93a386Sopenharmony_ci else: 381cb93a386Sopenharmony_ci printer(1, name) 382cb93a386Sopenharmony_ci for (indent, line) in formatted_result: 383cb93a386Sopenharmony_ci printer(indent, line) 384cb93a386Sopenharmony_ci if identical: 385cb93a386Sopenharmony_ci break 386cb93a386Sopenharmony_ci out.write('\n') 387cb93a386Sopenharmony_ci 388cb93a386Sopenharmony_ci if verbosity > 0: 389cb93a386Sopenharmony_ci # Print out summary of all of the bots. 390cb93a386Sopenharmony_ci out.write('%11s %11s %4s %s\n\n' % 391cb93a386Sopenharmony_ci ('CONTROL', 'ROLL', 'DIFF', 'BOT')) 392cb93a386Sopenharmony_ci for bot in sorted(all_bots): 393cb93a386Sopenharmony_ci if roll[bot].status == 'success': 394cb93a386Sopenharmony_ci diff = '' 395cb93a386Sopenharmony_ci elif (control[bot].status == 'success' and 396cb93a386Sopenharmony_ci roll[bot].status == 'failure'): 397cb93a386Sopenharmony_ci diff = '!!!!' 398cb93a386Sopenharmony_ci elif ('pending' in control[bot].status or 399cb93a386Sopenharmony_ci 'pending' in roll[bot].status): 400cb93a386Sopenharmony_ci diff = '....' 401cb93a386Sopenharmony_ci else: 402cb93a386Sopenharmony_ci diff = '****' 403cb93a386Sopenharmony_ci out.write('%11s %11s %4s %s\n' % ( 404cb93a386Sopenharmony_ci control[bot].status, roll[bot].status, diff, bot)) 405cb93a386Sopenharmony_ci out.write('\n') 406cb93a386Sopenharmony_ci out.flush() 407cb93a386Sopenharmony_ci 408cb93a386Sopenharmony_ciif __name__ == '__main__': 409cb93a386Sopenharmony_ci if len(sys.argv) < 3: 410cb93a386Sopenharmony_ci print >> sys.stderr, __doc__ 411cb93a386Sopenharmony_ci exit(1) 412cb93a386Sopenharmony_ci main(sys.argv[1], sys.argv[2], 413cb93a386Sopenharmony_ci int(os.environ.get('COMPARE_CODEREVIEW_VERBOSITY', 1))) 414cb93a386Sopenharmony_ci 415