Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(286)

Unified Diff: merger.py

Issue 12178026: Adds console renderer to merger backend. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/chromium-build
Patch Set: Responding to comments. Created 7 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « app.py ('k') | templates/merger_b.html » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: merger.py
diff --git a/merger.py b/merger.py
index 75dbd42cc5d13a7f228a2654c05bb8d37c5eaf05..32eda4920481b71659d15b8a498884cda662d403 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,18 +35,24 @@ 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()
+ def purge_unicode(self, enc='ascii', err='replace'):
+ self.committer = self.committer.encode(enc, err)
+ self.comment = self.comment.encode(enc, err)
+ self.details = self.details.encode(enc, err)
class MergerData(object):
"""Persistent data storage class.
@@ -55,13 +63,61 @@ 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):
+ d.setdefault(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]):
+ self.ordered_builders[m].setdefault(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]:
+ self.failures[m].setdefault(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 +125,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 +140,49 @@ 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']
+ row.status.setdefault(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]):
+ status_dict[master].setdefault(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 +190,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].purge_unicode()
+ 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),
« no previous file with comments | « app.py ('k') | templates/merger_b.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698