Chromium Code Reviews| Index: merger.py |
| diff --git a/merger.py b/merger.py |
| index 75dbd42cc5d13a7f228a2654c05bb8d37c5eaf05..4b312cf75214a6b084981f121139dc4114b18beb 100644 |
| --- a/merger.py |
| +++ b/merger.py |
| @@ -4,12 +4,14 @@ |
| import datetime |
| import logging |
| +import jinja2 |
| import webapp2 |
| import app |
| import base_page |
| import utils |
| +from third_party.BeautifulSoup.BeautifulSoup import BeautifulSoup |
| class BuildData(object): |
| """Represents a single build in the waterfall. |
| @@ -33,15 +35,17 @@ class BuildData(object): |
| class RowData(object): |
| """Represents a single row of the console. |
| - |
| + |
| Includes all individual builder statuses. |
| """ |
| def __init__(self): |
| self.revision = 0 |
| + self.revlink = None |
| self.committer = None |
| self.comment = None |
| self.details = None |
| + # Per-builder status stored at self.status[master][category][builder]. |
| self.status = {} |
| self.timestamp = datetime.datetime.now() |
| @@ -55,13 +59,64 @@ class MergerData(object): |
| def __init__(self): |
| self.SIZE = 100 |
| - self.rows = {} |
| + # Straight list of masters to display. |
| + self.ordered_masters = app.DEFAULT_MASTERS_TO_MERGE |
| + # Ordered categories, indexed by master. |
| + self.ordered_categories = {} |
| + # Ordered builders, indexed by master and category. |
| + self.ordered_builders = {} |
| self.latest_rev = 0 |
| - self.ordered_builders = [] |
| + self.rows = {} |
| + self.status = {} |
| self.failures = {} |
| def bootstrap(self): |
| """Fills an empty MergerData with 100 rows of data.""" |
| + # Populate the categories, masters, status, and failures data. |
| + for m in self.ordered_masters: |
| + for d in (self.ordered_builders, |
| + self.ordered_categories, |
| + self.status, |
| + self.failures): |
| + if m not in d: |
|
M-A Ruel
2013/02/08 02:16:05
d.setdefault(m, {})
|
| + d[m] = {} |
| + # Get the category data and construct the list of categories |
| + # for this master. |
| + category_data = app.get_and_cache_pagedata('%s/console/categories' % m) |
| + if not category_data['content']: |
| + category_list = [u'default'] |
| + else: |
| + category_soup = BeautifulSoup(category_data['content']) |
| + category_list = [tag.string.strip() for tag in |
| + category_soup.findAll('td', 'DevStatus')] |
| + self.ordered_categories[m] = category_list |
| + # Get the builder status data. |
| + builder_data = app.get_and_cache_pagedata('%s/console/summary' % m) |
| + if not builder_data['content']: |
| + continue |
| + builder_soup = BeautifulSoup(builder_data['content']) |
| + builders_by_category = builder_soup.tr.findAll('td', 'DevSlave', |
| + recursive=False) |
| + # Construct the list of builders for this category. |
| + for i, c in enumerate(self.ordered_categories[m]): |
| + if c not in self.ordered_builders[m]: |
| + self.ordered_builders[m][c] = {} |
| + builder_list = [tag['title'] for tag in |
| + builders_by_category[i].findAll('a', 'DevSlaveBox')] |
| + self.ordered_builders[m][c] = builder_list |
| + # Fill in the status data for all of this master's builders. |
| + update_status(m, builder_data['content'], self.status) |
| + # Copy that status data over into the failures dictionary too. |
| + for c in self.ordered_categories[m]: |
| + if c not in self.failures[m]: |
| + self.failures[m][c] = {} |
| + for b in self.ordered_builders[m][c]: |
| + if self.status[m][c][b] not in ('success', 'running', 'notstarted'): |
| + self.failures[m][c][b] = True |
| + else: |
| + self.failures[m][c][b] = False |
| + # Populate the individual row data, saving status info in the same |
| + # master/category/builder tree format constructed above. |
| latest_rev = int(app.get_and_cache_rowdata('latest_rev')['rev_number']) |
| if not latest_rev: |
| logging.error("MergerData.bootstrap(): Didn't get latest_rev. Aborting.") |
| @@ -69,27 +124,14 @@ class MergerData(object): |
| n = latest_rev |
| num_rows_saved = num_rows_skipped = 0 |
| while num_rows_saved < self.SIZE and num_rows_skipped < 10: |
| - logging.info('MergerData.bootstrap(): Getting revision %s' % n) |
| curr_row = RowData() |
| - for m in app.DEFAULT_MASTERS_TO_MERGE: |
| - # Fetch the relevant data from the datastore / cache. |
| - row_data = app.get_and_cache_rowdata('%s/console/%s' % (m, n)) |
| - if not row_data: |
| - continue |
| - # Only grab the common data from the main master. |
| - if m == 'chromium.main': |
| - curr_row.revision = int(row_data['rev_number']) |
| - curr_row.committer = row_data['name'] |
| - curr_row.comment = row_data['comment'] |
| - curr_row.details = row_data['details'] |
| - curr_row.status[m] = row_data['status'] |
| + for m in self.ordered_masters: |
| + update_row(n, m, curr_row) |
| # If we didn't get any data, that revision doesn't exist, so skip on. |
| if not curr_row.revision: |
| - logging.info('MergerData.bootstrap(): No data for revision %s' % n) |
| num_rows_skipped += 1 |
| n -= 1 |
| continue |
| - logging.info('MergerData.bootstrap(): Got data for revision %s' % n) |
| self.rows[n] = curr_row |
| num_rows_skipped = 0 |
| num_rows_saved += 1 |
| @@ -97,6 +139,51 @@ class MergerData(object): |
| self.latest_rev = max(self.rows.keys()) |
| +def update_row(revision, master, row): |
| + """Fetches a row from the datastore and puts it in a RowData object.""" |
| + # Fetch the relevant data from the datastore / cache. |
| + row_data = app.get_and_cache_rowdata('%s/console/%s' % (master, revision)) |
| + if not row_data: |
| + return |
| + # Only grab the common data from the main master. |
| + if master == 'chromium.main': |
| + row.revision = int(row_data['rev_number']) |
| + row.revlink = row_data['rev'] |
| + row.committer = row_data['name'] |
| + row.comment = row_data['comment'] |
| + row.details = row_data['details'] |
| + if master not in row.status: |
| + row.status[master] = {} |
| + update_status(master, row_data['status'], row.status) |
| + |
| + |
| +def update_status(master, status_html, status_dict): |
| + """Parses build status information and saves it to a status dictionary.""" |
| + builder_soup = BeautifulSoup(status_html) |
| + builders_by_category = builder_soup.findAll('table') |
| + for i, c in enumerate(data.ordered_categories[master]): |
| + if c not in status_dict[master]: |
| + status_dict[master][c] = {} |
| + statuses_by_builder = builders_by_category[i].findAll('td', |
| + 'DevStatusBox') |
| + # If we didn't get anything, it's because we're parsing the overall |
| + # summary, so look for Slave boxes instead of Status boxes. |
| + if not statuses_by_builder: |
| + statuses_by_builder = builders_by_category[i].findAll('td', |
| + 'DevSlaveBox') |
| + for j, b in enumerate(data.ordered_builders[master][c]): |
| + # Save the whole link as the status to keep ETA and build number info. |
| + status = unicode(statuses_by_builder[j].a) |
| + status_dict[master][c][b] = status |
| + |
| + |
| +def notstarted(status): |
| + """Converts a DevSlave status box to a notstarted DevStatus box.""" |
| + status_soup = BeautifulSoup(status) |
| + status_soup['class'] = 'DevStatusBox notstarted' |
| + return unicode(status_soup) |
| + |
| + |
| class MergerUpdateAction(base_page.BasePage): |
| """Handles update requests. |
| @@ -104,78 +191,74 @@ class MergerUpdateAction(base_page.BasePage): |
| """ |
| def get(self): |
| - logging.info("***BACKEND MERGER UPDATE***") |
| - logging.info('BEGIN Stored rows are: %s' % sorted(data.rows)) |
| latest_rev = int(app.get_and_cache_rowdata('latest_rev')['rev_number']) |
| - logging.info('Merger.update(): latest_rev = %s' % latest_rev) |
| # We may have brand new rows, so store them. |
| if latest_rev not in data.rows: |
| - logging.info('Merger.update(): Handling new rows.') |
| for n in xrange(data.latest_rev + 1, latest_rev + 1): |
| - logging.info('Merger.update(): Getting revision %s' % n) |
| curr_row = RowData() |
| - for m in app.DEFAULT_MASTERS_TO_MERGE: |
| - # Fetch the relevant data from the datastore / cache. |
| - row_data = app.get_and_cache_rowdata('%s/console/%s' % (m, n)) |
| - if not row_data: |
| - continue |
| - # Only grab the common data from the main master. |
| - if m == 'chromium.main': |
| - curr_row.revision = int(row_data['rev_number']) |
| - curr_row.committer = row_data['name'] |
| - curr_row.comment = row_data['comment'] |
| - curr_row.details = row_data['details'] |
| - curr_row.status[m] = row_data['status'] |
| + for m in data.ordered_masters: |
| + update_row(n, m, curr_row) |
| # If we didn't get any data, that revision doesn't exist, so skip on. |
| if not curr_row.revision: |
| - logging.info('Merger.update(): No data for revision %s' % n) |
| continue |
| - logging.info('Merger.update(): Got data for revision %s' % n) |
| data.rows[n] = curr_row |
| # Update our stored latest_rev to reflect the new data. |
| data.latest_rev = max(data.rows.keys()) |
| # Now update the status of the rest of the rows. |
| offset = 0 |
| - logging.info('Merger.update(): Updating rows.') |
| while offset < data.SIZE: |
| n = data.latest_rev - offset |
| - logging.info('Merger.update(): Checking revision %s' % n) |
| if n not in data.rows: |
| - logging.info('Merger.update(): Don\'t care about revision %s' % n) |
| offset += 1 |
| continue |
| curr_row = data.rows[n] |
| - for m in app.DEFAULT_MASTERS_TO_MERGE: |
| + for m in data.ordered_masters: |
| row_data = app.get_and_cache_rowdata('%s/console/%s' % (m, n)) |
| if not row_data: |
| continue |
| - curr_row.status[m] = row_data['status'] |
| + update_status(m, row_data['status'], curr_row.status) |
| offset += 1 |
| - logging.info('Merger.update(): Got new data for revision %s' % n) |
| # Finally delete any extra rows that we don't want to keep around. |
| if len(data.rows) > data.SIZE: |
| - old_revs = sorted(data.rows, reverse=True)[data.SIZE:] |
| - logging.info('Merger.update(): Deleting rows %s' % old_revs) |
| + old_revs = sorted(data.rows.keys(), reverse=True)[data.SIZE:] |
| for rev in old_revs: |
| del data.rows[rev] |
| - logging.info('FINAL Stored rows are: %s' % sorted(data.rows)) |
| - self.response.out.write('Update completed.') |
| + self.response.out.write('Update completed (rows %s - %s).' % |
| + (min(data.rows.keys()), max(data.rows.keys()))) |
| class MergerRenderAction(base_page.BasePage): |
| def get(self): |
| + class TemplateData(object): |
| + def __init__(self, rhs, numrevs): |
| + self.ordered_rows = sorted(rhs.rows.keys(), reverse=True)[:numrevs] |
| + self.ordered_masters = rhs.ordered_masters |
| + self.ordered_categories = rhs.ordered_categories |
| + self.ordered_builders = rhs.ordered_builders |
| + self.status = rhs.status |
| + self.rows = {} |
| + for row in self.ordered_rows: |
| + self.rows[row] = rhs.rows[row] |
| + self.category_count = sum([len(self.ordered_categories[master]) |
| + for master in self.ordered_masters]) |
| num_revs = self.request.get('numrevs') |
| if num_revs: |
| num_revs = utils.clean_int(num_revs, -1) |
| if not num_revs or num_revs <= 0: |
| num_revs = 25 |
| - self.response.out.write('Render not yet implemented (%s rows).' % num_revs) |
| + out = TemplateData(data, num_revs) |
| + template = template_environment.get_template('merger_b.html') |
| + self.response.out.write(template.render(data=out)) |
| # Summon our persistent data model into existence. |
| data = MergerData() |
| data.bootstrap() |
| +template_environment = jinja2.Environment() |
| +template_environment.loader = jinja2.FileSystemLoader('templates') |
| +template_environment.filters['notstarted'] = notstarted |
| + |
| URLS = [ |
| ('/restricted/merger/update', MergerUpdateAction), |