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