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

Side by Side 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: 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 unified diff | Download patch | Annotate | Revision Log
« app.py ('K') | « app.py ('k') | templates/merger_b.html » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be 2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file. 3 # found in the LICENSE file.
4 4
5 import datetime 5 import datetime
6 import logging 6 import logging
7 import jinja2
7 import webapp2 8 import webapp2
8 9
9 import app 10 import app
10 import base_page 11 import base_page
11 import utils 12 import utils
12 13
14 from third_party.BeautifulSoup.BeautifulSoup import BeautifulSoup
13 15
14 class BuildData(object): 16 class BuildData(object):
15 """Represents a single build in the waterfall. 17 """Represents a single build in the waterfall.
16 18
17 Not yet used (this backend only renders a console so far). 19 Not yet used (this backend only renders a console so far).
18 TODO(agable): Use, and include step-level info. 20 TODO(agable): Use, and include step-level info.
19 """ 21 """
20 22
21 STATUS_ENUM = ( 23 STATUS_ENUM = (
22 'notstarted', 24 'notstarted',
23 'running', 25 'running',
24 'success', 26 'success',
25 'warnings', 27 'warnings',
26 'failure', 28 'failure',
27 'exception', 29 'exception',
28 ) 30 )
29 31
30 def __init__(self): 32 def __init__(self):
31 self.status = 0 33 self.status = 0
32 34
33 35
34 class RowData(object): 36 class RowData(object):
35 """Represents a single row of the console. 37 """Represents a single row of the console.
36 38
37 Includes all individual builder statuses. 39 Includes all individual builder statuses.
38 """ 40 """
39 41
40 def __init__(self): 42 def __init__(self):
41 self.revision = 0 43 self.revision = 0
44 self.revlink = None
42 self.committer = None 45 self.committer = None
43 self.comment = None 46 self.comment = None
44 self.details = None 47 self.details = None
48 # Per-builder status stored at self.status[master][category][builder].
45 self.status = {} 49 self.status = {}
46 self.timestamp = datetime.datetime.now() 50 self.timestamp = datetime.datetime.now()
47 51
48 52
49 class MergerData(object): 53 class MergerData(object):
50 """Persistent data storage class. 54 """Persistent data storage class.
51 55
52 Holds all of the data we have about the last 100 revisions. 56 Holds all of the data we have about the last 100 revisions.
53 Keeps it organized and can render it upon request. 57 Keeps it organized and can render it upon request.
54 """ 58 """
55 59
56 def __init__(self): 60 def __init__(self):
57 self.SIZE = 100 61 self.SIZE = 100
62 # Straight list of masters to display.
63 self.ordered_masters = app.DEFAULT_MASTERS_TO_MERGE
64 # Ordered categories, indexed by master.
65 self.ordered_categories = {}
66 # Ordered builders, indexed by master and category.
67 self.ordered_builders = {}
68 self.latest_rev = 0
58 self.rows = {} 69 self.rows = {}
59 self.latest_rev = 0 70 self.status = {}
60 self.ordered_builders = []
61 self.failures = {} 71 self.failures = {}
62 72
63 def bootstrap(self): 73 def bootstrap(self):
64 """Fills an empty MergerData with 100 rows of data.""" 74 """Fills an empty MergerData with 100 rows of data."""
75 # Populate the categories, masters, status, and failures data.
76 for m in self.ordered_masters:
77 for d in (self.ordered_builders,
78 self.ordered_categories,
79 self.status,
80 self.failures):
81 if m not in d:
M-A Ruel 2013/02/08 02:16:05 d.setdefault(m, {})
82 d[m] = {}
83 # Get the category data and construct the list of categories
84 # for this master.
85 category_data = app.get_and_cache_pagedata('%s/console/categories' % m)
86 if not category_data['content']:
87 category_list = [u'default']
88 else:
89 category_soup = BeautifulSoup(category_data['content'])
90 category_list = [tag.string.strip() for tag in
91 category_soup.findAll('td', 'DevStatus')]
92 self.ordered_categories[m] = category_list
93 # Get the builder status data.
94 builder_data = app.get_and_cache_pagedata('%s/console/summary' % m)
95 if not builder_data['content']:
96 continue
97 builder_soup = BeautifulSoup(builder_data['content'])
98 builders_by_category = builder_soup.tr.findAll('td', 'DevSlave',
99 recursive=False)
100 # Construct the list of builders for this category.
101 for i, c in enumerate(self.ordered_categories[m]):
102 if c not in self.ordered_builders[m]:
103 self.ordered_builders[m][c] = {}
104 builder_list = [tag['title'] for tag in
105 builders_by_category[i].findAll('a', 'DevSlaveBox')]
106 self.ordered_builders[m][c] = builder_list
107 # Fill in the status data for all of this master's builders.
108 update_status(m, builder_data['content'], self.status)
109 # Copy that status data over into the failures dictionary too.
110 for c in self.ordered_categories[m]:
111 if c not in self.failures[m]:
112 self.failures[m][c] = {}
113 for b in self.ordered_builders[m][c]:
114 if self.status[m][c][b] not in ('success', 'running', 'notstarted'):
115 self.failures[m][c][b] = True
116 else:
117 self.failures[m][c][b] = False
118 # Populate the individual row data, saving status info in the same
119 # master/category/builder tree format constructed above.
65 latest_rev = int(app.get_and_cache_rowdata('latest_rev')['rev_number']) 120 latest_rev = int(app.get_and_cache_rowdata('latest_rev')['rev_number'])
66 if not latest_rev: 121 if not latest_rev:
67 logging.error("MergerData.bootstrap(): Didn't get latest_rev. Aborting.") 122 logging.error("MergerData.bootstrap(): Didn't get latest_rev. Aborting.")
68 return 123 return
69 n = latest_rev 124 n = latest_rev
70 num_rows_saved = num_rows_skipped = 0 125 num_rows_saved = num_rows_skipped = 0
71 while num_rows_saved < self.SIZE and num_rows_skipped < 10: 126 while num_rows_saved < self.SIZE and num_rows_skipped < 10:
72 logging.info('MergerData.bootstrap(): Getting revision %s' % n)
73 curr_row = RowData() 127 curr_row = RowData()
74 for m in app.DEFAULT_MASTERS_TO_MERGE: 128 for m in self.ordered_masters:
75 # Fetch the relevant data from the datastore / cache. 129 update_row(n, m, curr_row)
76 row_data = app.get_and_cache_rowdata('%s/console/%s' % (m, n))
77 if not row_data:
78 continue
79 # Only grab the common data from the main master.
80 if m == 'chromium.main':
81 curr_row.revision = int(row_data['rev_number'])
82 curr_row.committer = row_data['name']
83 curr_row.comment = row_data['comment']
84 curr_row.details = row_data['details']
85 curr_row.status[m] = row_data['status']
86 # If we didn't get any data, that revision doesn't exist, so skip on. 130 # If we didn't get any data, that revision doesn't exist, so skip on.
87 if not curr_row.revision: 131 if not curr_row.revision:
88 logging.info('MergerData.bootstrap(): No data for revision %s' % n)
89 num_rows_skipped += 1 132 num_rows_skipped += 1
90 n -= 1 133 n -= 1
91 continue 134 continue
92 logging.info('MergerData.bootstrap(): Got data for revision %s' % n)
93 self.rows[n] = curr_row 135 self.rows[n] = curr_row
94 num_rows_skipped = 0 136 num_rows_skipped = 0
95 num_rows_saved += 1 137 num_rows_saved += 1
96 n -= 1 138 n -= 1
97 self.latest_rev = max(self.rows.keys()) 139 self.latest_rev = max(self.rows.keys())
98 140
99 141
142 def update_row(revision, master, row):
143 """Fetches a row from the datastore and puts it in a RowData object."""
144 # Fetch the relevant data from the datastore / cache.
145 row_data = app.get_and_cache_rowdata('%s/console/%s' % (master, revision))
146 if not row_data:
147 return
148 # Only grab the common data from the main master.
149 if master == 'chromium.main':
150 row.revision = int(row_data['rev_number'])
151 row.revlink = row_data['rev']
152 row.committer = row_data['name']
153 row.comment = row_data['comment']
154 row.details = row_data['details']
155 if master not in row.status:
156 row.status[master] = {}
157 update_status(master, row_data['status'], row.status)
158
159
160 def update_status(master, status_html, status_dict):
161 """Parses build status information and saves it to a status dictionary."""
162 builder_soup = BeautifulSoup(status_html)
163 builders_by_category = builder_soup.findAll('table')
164 for i, c in enumerate(data.ordered_categories[master]):
165 if c not in status_dict[master]:
166 status_dict[master][c] = {}
167 statuses_by_builder = builders_by_category[i].findAll('td',
168 'DevStatusBox')
169 # If we didn't get anything, it's because we're parsing the overall
170 # summary, so look for Slave boxes instead of Status boxes.
171 if not statuses_by_builder:
172 statuses_by_builder = builders_by_category[i].findAll('td',
173 'DevSlaveBox')
174 for j, b in enumerate(data.ordered_builders[master][c]):
175 # Save the whole link as the status to keep ETA and build number info.
176 status = unicode(statuses_by_builder[j].a)
177 status_dict[master][c][b] = status
178
179
180 def notstarted(status):
181 """Converts a DevSlave status box to a notstarted DevStatus box."""
182 status_soup = BeautifulSoup(status)
183 status_soup['class'] = 'DevStatusBox notstarted'
184 return unicode(status_soup)
185
186
100 class MergerUpdateAction(base_page.BasePage): 187 class MergerUpdateAction(base_page.BasePage):
101 """Handles update requests. 188 """Handles update requests.
102 189
103 Takes data gathered by the cronjob and pulls it into active memory. 190 Takes data gathered by the cronjob and pulls it into active memory.
104 """ 191 """
105 192
106 def get(self): 193 def get(self):
107 logging.info("***BACKEND MERGER UPDATE***")
108 logging.info('BEGIN Stored rows are: %s' % sorted(data.rows))
109 latest_rev = int(app.get_and_cache_rowdata('latest_rev')['rev_number']) 194 latest_rev = int(app.get_and_cache_rowdata('latest_rev')['rev_number'])
110 logging.info('Merger.update(): latest_rev = %s' % latest_rev)
111 # We may have brand new rows, so store them. 195 # We may have brand new rows, so store them.
112 if latest_rev not in data.rows: 196 if latest_rev not in data.rows:
113 logging.info('Merger.update(): Handling new rows.')
114 for n in xrange(data.latest_rev + 1, latest_rev + 1): 197 for n in xrange(data.latest_rev + 1, latest_rev + 1):
115 logging.info('Merger.update(): Getting revision %s' % n)
116 curr_row = RowData() 198 curr_row = RowData()
117 for m in app.DEFAULT_MASTERS_TO_MERGE: 199 for m in data.ordered_masters:
118 # Fetch the relevant data from the datastore / cache. 200 update_row(n, m, curr_row)
119 row_data = app.get_and_cache_rowdata('%s/console/%s' % (m, n))
120 if not row_data:
121 continue
122 # Only grab the common data from the main master.
123 if m == 'chromium.main':
124 curr_row.revision = int(row_data['rev_number'])
125 curr_row.committer = row_data['name']
126 curr_row.comment = row_data['comment']
127 curr_row.details = row_data['details']
128 curr_row.status[m] = row_data['status']
129 # If we didn't get any data, that revision doesn't exist, so skip on. 201 # If we didn't get any data, that revision doesn't exist, so skip on.
130 if not curr_row.revision: 202 if not curr_row.revision:
131 logging.info('Merger.update(): No data for revision %s' % n)
132 continue 203 continue
133 logging.info('Merger.update(): Got data for revision %s' % n)
134 data.rows[n] = curr_row 204 data.rows[n] = curr_row
135 # Update our stored latest_rev to reflect the new data. 205 # Update our stored latest_rev to reflect the new data.
136 data.latest_rev = max(data.rows.keys()) 206 data.latest_rev = max(data.rows.keys())
137 # Now update the status of the rest of the rows. 207 # Now update the status of the rest of the rows.
138 offset = 0 208 offset = 0
139 logging.info('Merger.update(): Updating rows.')
140 while offset < data.SIZE: 209 while offset < data.SIZE:
141 n = data.latest_rev - offset 210 n = data.latest_rev - offset
142 logging.info('Merger.update(): Checking revision %s' % n)
143 if n not in data.rows: 211 if n not in data.rows:
144 logging.info('Merger.update(): Don\'t care about revision %s' % n)
145 offset += 1 212 offset += 1
146 continue 213 continue
147 curr_row = data.rows[n] 214 curr_row = data.rows[n]
148 for m in app.DEFAULT_MASTERS_TO_MERGE: 215 for m in data.ordered_masters:
149 row_data = app.get_and_cache_rowdata('%s/console/%s' % (m, n)) 216 row_data = app.get_and_cache_rowdata('%s/console/%s' % (m, n))
150 if not row_data: 217 if not row_data:
151 continue 218 continue
152 curr_row.status[m] = row_data['status'] 219 update_status(m, row_data['status'], curr_row.status)
153 offset += 1 220 offset += 1
154 logging.info('Merger.update(): Got new data for revision %s' % n)
155 # Finally delete any extra rows that we don't want to keep around. 221 # Finally delete any extra rows that we don't want to keep around.
156 if len(data.rows) > data.SIZE: 222 if len(data.rows) > data.SIZE:
157 old_revs = sorted(data.rows, reverse=True)[data.SIZE:] 223 old_revs = sorted(data.rows.keys(), reverse=True)[data.SIZE:]
158 logging.info('Merger.update(): Deleting rows %s' % old_revs)
159 for rev in old_revs: 224 for rev in old_revs:
160 del data.rows[rev] 225 del data.rows[rev]
161 logging.info('FINAL Stored rows are: %s' % sorted(data.rows)) 226 self.response.out.write('Update completed (rows %s - %s).' %
162 self.response.out.write('Update completed.') 227 (min(data.rows.keys()), max(data.rows.keys())))
163 228
164 229
165 class MergerRenderAction(base_page.BasePage): 230 class MergerRenderAction(base_page.BasePage):
166 231
167 def get(self): 232 def get(self):
233 class TemplateData(object):
234 def __init__(self, rhs, numrevs):
235 self.ordered_rows = sorted(rhs.rows.keys(), reverse=True)[:numrevs]
236 self.ordered_masters = rhs.ordered_masters
237 self.ordered_categories = rhs.ordered_categories
238 self.ordered_builders = rhs.ordered_builders
239 self.status = rhs.status
240 self.rows = {}
241 for row in self.ordered_rows:
242 self.rows[row] = rhs.rows[row]
243 self.category_count = sum([len(self.ordered_categories[master])
244 for master in self.ordered_masters])
168 num_revs = self.request.get('numrevs') 245 num_revs = self.request.get('numrevs')
169 if num_revs: 246 if num_revs:
170 num_revs = utils.clean_int(num_revs, -1) 247 num_revs = utils.clean_int(num_revs, -1)
171 if not num_revs or num_revs <= 0: 248 if not num_revs or num_revs <= 0:
172 num_revs = 25 249 num_revs = 25
173 self.response.out.write('Render not yet implemented (%s rows).' % num_revs) 250 out = TemplateData(data, num_revs)
251 template = template_environment.get_template('merger_b.html')
252 self.response.out.write(template.render(data=out))
174 253
175 254
176 # Summon our persistent data model into existence. 255 # Summon our persistent data model into existence.
177 data = MergerData() 256 data = MergerData()
178 data.bootstrap() 257 data.bootstrap()
258 template_environment = jinja2.Environment()
259 template_environment.loader = jinja2.FileSystemLoader('templates')
260 template_environment.filters['notstarted'] = notstarted
261
179 262
180 URLS = [ 263 URLS = [
181 ('/restricted/merger/update', MergerUpdateAction), 264 ('/restricted/merger/update', MergerUpdateAction),
182 ('/restricted/merger/render.*', MergerRenderAction), 265 ('/restricted/merger/render.*', MergerRenderAction),
183 ] 266 ]
184 267
185 application = webapp2.WSGIApplication(URLS, debug=True) 268 application = webapp2.WSGIApplication(URLS, debug=True)
OLDNEW
« app.py ('K') | « app.py ('k') | templates/merger_b.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698