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

Side by Side Diff: buildlogparse.py

Issue 13892003: Added buildbot appengine frontend for chromium-build app (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/chromium-build
Patch Set: Created 7 years, 8 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
OLDNEW
(Empty)
1
2
3 import webapp2
4 from google.appengine.ext import db
5 from datetime import timedelta
6 import cStringIO
7 import time
8 import jinja2
9 import datetime
10 import re
11 import logging
12 import urllib
13 from google.appengine.api import urlfetch
14 import base64
15 import urlparse
16 import os
17 import json
18 import Queue
19 import os
20 import zlib
21 from google.appengine.api import users
22 from google.appengine.api import memcache
23 from google.appengine.ext import deferred
24 from google.appengine.api import files
25 from google.appengine.api import mail
agable 2013/04/15 19:33:30 Please cleanup imports to be only what you actuall
Ryan Tseng 2013/04/17 22:53:48 Done.
Ryan Tseng 2013/04/17 22:53:48 Done.
26
27 VERSION_ID = os.environ['CURRENT_VERSION_ID']
28
29 jinja_environment = jinja2.Environment(
30 loader=jinja2.FileSystemLoader(os.path.join(os.path.dirname(__file__),
31 'templates')),
32 autoescape=True,
33 extensions=['jinja2.ext.autoescape'])
34
35 if os.environ.get('HTTP_HOST'):
36 APP_URL = os.environ['HTTP_HOST']
37 else:
38 APP_URL = os.environ['SERVER_NAME']
39
40 REPLACEMENTS = [
41 # Find ../../scripts/.../*.py scripts and add links to them.
42 (r'\.\./\.\./\.\./scripts/(.*)\.py',
43 r'<a href="https://code.google.com/p/chromium/codesearch#chromium/tools/'
44 r'build/scripts/\1.py">../../scripts/\1.py</a>'),
45
46 # Find ../../chrome/.../*.cc files and add links to them.
47 (r'\.\./\.\./chrome/(.*)\.cc:(\d+)',
48 r'<a href="https://code.google.com/p/chromium/codesearch#chromium/src/'
49 r'chrome/\1.cc&l=\2">../../chrome/\1.cc:\2</a>'),
50
51 # Searches for codereview issue numbers, and add codereview links.
52 (r'apply_issue(.*)-i&nbsp;(\d{8})(.*)-s&nbsp;(.*)',
53 r'apply_issue\1-i&nbsp;<a href="\4/\2">\2</a>\3-s&nbsp;\4'),
54
55 # Add green labels to PASSED items.
56 (r'\[(&nbsp;&nbsp;PASSED&nbsp;&nbsp;)\]',
57 r'<span class="label label-success">[\1]</span>'),
58
59 # Add red labels to FAILED items.
60 (r'\[(&nbsp;&nbsp;FAILED&nbsp;&nbsp;)\]',
61 r'<span class="label label-important">[\1]</span>'),
62
63 # Add black labels ot RUN items.
64 (r'\[(&nbsp;RUN&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;)\]',
65 r'<span class="label label-inverse">[\1]</span>'),
66
67 # Add badges to running tests.
68 (r'\[((&nbsp;)*\d+/\d+)\]((&nbsp;)+)(\d+\.\d+s)&nbsp;'
69 r'([\w/]+\.[\w/]+)&nbsp;\(([\d.s]+)\)',
70 r'<span class="badge badge-success">\1</span>\3<span class="badge">'
71 r'\5</span>&nbsp;\6&nbsp;<span class="badge">\7</span>'),
72
73 # Add gray labels to [==========] blocks.
74 (r'\[([-=]{10})\]',
75 r'<span class="label">[\1]</span>'),
76
77 # Find .cc and .h files and add codesite links to them.
78 (r'\.\./\.\./([\w/-]+)\.(cc|h):&nbsp;',
79 r'<a href="https://code.google.com/p/chromium/codesearch#chromium/src/'
80 r'\1.\2">../../\1.\2</a>:&nbsp;'),
81
82 # Find source files with line numbers and add links to them.
83 (r'\.\./\.\./([\w/-]+)\.(cc|h):(\d+):&nbsp;',
84 r'<a href="https://code.google.com/p/chromium/codesearch#chromium/src/'
85 r'\1.\2&l=\3">../../\1.\2:\3</a>:&nbsp;'),
86
87 # Add badges to compiling items.
88 (r'\[(\d+/\d+)\]&nbsp;(CXX|AR|STAMP|CC|ACTION|RULE|COPY)',
89 r'<span class="badge badge-info">\1</span>&nbsp;'
90 r'<span class="badge">\2</span>'),
91
92 # Bold the LHS of A=B text.
93 (r'^((&nbsp;)*)(\w+)=([\w:/-_.]+)',
94 r'\1<strong>\3</strong>=\4'),
95 ]
96
97 ###############
98 # Jinja filters
99 ###############
100
101 def delta_time(delta):
102 hours = int(delta/60/60)
103 minutes = int((delta - hours * 3600)/60)
104 seconds = int(delta - (hours * 3600) - (minutes * 60))
105 result = ''
106 if hours > 1:
107 result += '%d hrs ' % hours
agable 2013/04/15 19:33:30 nit: add commas after hr/hrs, min/mins, and a peri
Ryan Tseng 2013/04/17 22:53:48 Done.
108 elif hours:
109 result += '%d hr ' % hours
110 if minutes > 1:
111 result += '%d mins ' % minutes
112 elif minutes:
113 result += '%d min ' % minutes
114 if not hours:
115 if seconds > 1:
116 result += '%d secs' % seconds
117 else:
118 result += '%d sec' % seconds
119 return result
120 jinja_environment.filters['delta_time'] = delta_time
121
122 def time_since(timestamp):
123 delta = time.time() - timestamp
124 return delta_time(delta)
125 jinja_environment.filters['time_since'] = time_since
126
127 def nl2br(value):
128 return value.replace('\n','<br>\n')
129 jinja_environment.filters['nl2br'] = nl2br
130
131 def cl_comment(value):
132 """Add links to https:// addresses, BUG=####, and trim excessive newlines."""
133 value = re.sub(r'(https?://.*)', r'<a href="\1">\1</a>', value)
134 value = re.sub(
135 r'BUG=(\d+)', r'BUG=<a href="http://crbug.com/\1">\1</a>', value)
136 # value = re.sub(r'\n\n', r'\n', value)
137 value = re.sub(r'\n', r'<br>', value)
138 return value
139 jinja_environment.filters['cl_comment'] = cl_comment
140
141 ########
142 # Models
143 ########
144
145 class BuildLogModel(db.Model):
146 # Used for caching finished build logs.
147 url = db.StringProperty()
148 data = db.BlobProperty()
149
150 class BuildLogResultModel(db.Model):
151 # Used for caching finished and parsed build logs.
152 url = db.StringProperty()
153 version = db.StringProperty()
154 data = db.BlobProperty()
155
156
157 ############
158 # Decorators
159 ############
160 def render(template_filename):
agable 2013/04/15 19:33:30 Docstring, similar to the one for render_json belo
Ryan Tseng 2013/04/17 22:53:48 Done.
161 def _render(fn):
162 def wrapper(self, *args, **kwargs):
163 results = fn(self, *args, **kwargs)
164 template = jinja_environment.get_template(template_filename)
165 self.response.out.write(template.render(results))
166 return wrapper
167 return _render
168
169 def render_json(fn):
170 # The function is expected to return a dict, and we want to render json.
agable 2013/04/15 19:33:30 Make this a real docstring.
Ryan Tseng 2013/04/17 22:53:48 Done.
171 def wrapper(self, *args, **kwargs):
172 results = fn(self, *args, **kwargs)
173 self.response.out.write(json.dumps(results))
174 return wrapper
175
176 def return_json_if_flag_is_set_else_render(template_filename):
agable 2013/04/15 19:33:30 maybe_return_json?
Ryan Tseng 2013/04/17 22:53:48 Works
177 """If the variable 'json' exists in the request, return a json object.
178 Otherwise render the page using the template"""
179 def _render(fn):
180 def wrapper(self, *args, **kwargs):
181 results = fn(self, *args, **kwargs)
182 if self.request.get('json'):
183 self.response.out.write(json.dumps(results))
184 else:
185 template = jinja_environment.get_template(template_filename)
186 self.response.out.write(template.render(results))
187 return wrapper
188 return _render
189
190 def login_required(fn):
191 """Redirect user to a login page."""
192 def wrapper(self, *args, **kwargs):
193 user = users.get_current_user()
194 if not user:
195 self.redirect(users.create_login_url(self.request.uri))
196 return
197 else:
198 return fn(self, *args, **kwargs)
199 return wrapper
200
201 def google_login_required(fn):
202 """Return 403 unless the user is logged in from a @google.com domain"""
203 def wrapper(self, *args, **kwargs):
204 user = users.get_current_user()
205 if not user:
206 self.redirect(users.create_login_url(self.request.uri))
207 return
208 email_match = re.match('^(.*)@(.*)$', user.email())
209 if email_match:
210 _, domain = email_match.groups()
211 if domain == 'google.com':
212 return fn(self, *args, **kwargs)
213 self.error(403) # Unrecognized email or unauthroized domain.
214 self.response.out.write('unauthroized email %s' % user.user_id())
215 return wrapper
216
217 def admin_required(fn):
218 """Return 403 unless an admin is logged in"""
agable 2013/04/15 19:33:30 Give all of these docstrings periods -- they're se
Ryan Tseng 2013/04/17 22:53:48 Done.
219 def wrapper(self, *args, **kwargs):
220 user = users.get_current_user()
221 if not user:
222 self.redirect(users.create_login_url(self.request.uri))
223 return
224 elif not users.is_current_user_admin():
225 self.error(403)
226 return
227 else:
228 return fn(self, *args, **kwargs)
229 return wrapper
230
231 def expect_request(*request_args):
agable 2013/04/15 19:33:30 expect_request_param? expect_request sounds like i
Ryan Tseng 2013/04/17 22:53:48 Works for me. Done
232 """Strips out the expected args from a request and feeds it into the function
233 as the arguments. Optionally, typecast the argument from a string into a
234 different class. Examples include:
235 name (Get the request object called "name")
236 time as timestamp (Get "time", pass it in as "timestamp")
237 """
238 def _decorator(fn):
239 def wrapper(self, *args, **kwargs):
240 request_kwargs = {}
241 for arg in request_args:
242 arg_match = re.match(r'^(\((\w+)\))?\s*(\w+)( as (\w+))?$', arg)
243 if arg_match:
244 _, target_type_name, name, _, target_name = arg_match.groups()
245 if not target_name:
246 target_name = name
247 request_item = self.request.get(name)
248 request_kwargs[target_name] = request_item
249 else:
250 raise Exception('Incorrect format %s' % arg)
251 kwargs.update(request_kwargs)
252 return fn(self, *args, **kwargs)
253 return wrapper
254 return _decorator
agable 2013/04/15 19:33:30 All these wrappers are really nice and general. On
Ryan Tseng 2013/04/17 22:53:48 Or I can do that now :)
255
256 def emit(source, out):
257 # TODO(hinoka): This currently employs a "lookback" strategy
258 # (Find [PASS/FAIL], then goes back and marks all of the lines.)
259 # This should be switched to a "scan twice" strategy. 1st pass creates a
260 # Test Name -> PASS/FAIL/INCOMPLETE dictionary, and 2nd pass marks the lines.
261 title = source
agable 2013/04/15 19:33:30 Remove this, title is never used.
Ryan Tseng 2013/04/17 22:53:48 Done.
262 attr = []
263 if source == 'header':
264 attr.append('text-info')
265 lines = []
266 current_test = None
267 current_test_line = 0
268 for line in out.split('\n'):
269 if line:
270 test_match = re.search(r'\[ RUN \]\s*([^() ]*)\s*', line)
agable 2013/04/15 19:33:30 Here you're searching for [ RUN ], while earli
Ryan Tseng 2013/04/17 22:53:48 This set of regex is a bit special in that its not
271 line_attr = attr[:]
272 if test_match:
273 # This line is a "We're running a test" line.
274 current_test = test_match.group(1).strip()
275 current_test_line = len(lines)
276 elif '[ OK ]' in line or '[ PASSED ]' in line:
277 line_attr.append('text-success')
278 test_match = re.search(r'\[ OK \]\s*([^(), ]*)\s*', line)
279 if test_match:
280 finished_test = test_match.group(1).strip()
281 for line_item in lines[current_test_line:]:
282 if finished_test == current_test:
283 line_item[2].append('text-success')
284 else:
285 line_item[2].append('text-error')
286 current_test = None
287 elif '[ FAILED ]' in line:
288 line_attr.append('text-error')
289 test_match = re.search(r'\[ FAILED \]\s*([^(), ]*)\s*', line)
290 if test_match:
291 finished_test = test_match.group(1).strip()
292 for line_item in lines[current_test_line:]:
293 if finished_test == current_test:
294 line_item[2].append('text-error')
295 current_test = None
296 elif re.search(r'\[.{10}\]', line):
297 current_test = None
298 elif re.search(r'\[\s*\d+/\d+\]\s*\d+\.\d+s\s+[\w/]+\.'
agable 2013/04/15 19:33:30 Document your regexes :)
Ryan Tseng 2013/04/17 22:53:48 Done.
299 r'[\w/]+\s+\([\d.s]+\)', line):
300 current_test = None
301 line_attr.append('text-success')
302 elif 'aborting test' in line:
303 current_test = None
304 elif current_test:
305 line_attr.append('text-warning')
306
307 if len(line) > 160:
agable 2013/04/15 19:33:30 Why 160?
Ryan Tseng 2013/04/17 22:53:48 That was arbitrary. I think I'll remove this and
308 line_abbr = line[:160]
309 line_abbr = line_abbr.replace(' ', '&nbsp;')
310 line = line.replace(' ', '&nbsp;')
311 if 'apply_issue' in line:
312 logging.warning(line)
313 for rep_from, rep_to in REPLACEMENTS:
314 line_abbr = re.sub(rep_from, rep_to, line_abbr)
315 line = re.sub(rep_from, rep_to, line)
316 lines.append((line_abbr, line, line_attr))
317 else:
318 line = line.replace(' ', '&nbsp;')
319 for rep_from, rep_to in REPLACEMENTS:
320 line = re.sub(rep_from, rep_to, line)
321 lines.append((None, line, line_attr))
agable 2013/04/15 19:33:30 Can pull this duplicated code (line.replace; for f
Ryan Tseng 2013/04/17 22:53:48 Removed line_abbr anyways.
322 return (title, lines)
agable 2013/04/15 19:33:30 Remove 'return title', it is identical to the inpu
Ryan Tseng 2013/04/17 22:53:48 Done.
323
324
325 class BuildStep(webapp2.RequestHandler):
326 """Prases a build step page."""
agable 2013/04/15 19:33:30 Parses. He how prases the build step page.
Ryan Tseng 2013/04/17 22:53:48 Done.
327 @render('step.html')
328 @expect_request('url')
329 def get(self, url):
330 if not url:
331 self.redirect('/buildbot/')
agable 2013/04/15 19:33:30 See comment below about having url be a required u
Ryan Tseng 2013/04/17 22:53:48 Done.
332
333 # Fetch the page.
334 sch, netloc, path, _, _, _ = urlparse.urlparse(url)
335 url_m = re.match(r'^/((p/)?)(.*)/builders/(.*)/builds/(\d+)$', path)
agable 2013/04/15 19:33:30 Offline comment about this (p/)? to follow.
Ryan Tseng 2013/04/17 22:53:48 ?
336 if not url_m:
337 self.redirect('/buildbot/')
338 prefix, _, master, builder, step = url_m.groups()
339 json_url = '%s://%s/%s%s/json/builders/%s/builds/%s' % (
340 sch, netloc, prefix, master, builder, step)
341 s = urlfetch.fetch(json_url.replace(' ', '%20'),
342 method=urlfetch.GET, deadline=60).content
343 logging.info(s)
344
345 result = json.loads(s)
346
347 # Add on some extraneous info.
348 build_properties = dict((name, value) for name, value, _
349 in result['properties'])
350
351 if 'rietveld' in build_properties:
352 result['rietveld'] = build_properties['rietveld']
353 result['breadcrumbs'] = [
354 ('Master %s' % master, '#'),
355 ('Builder %s' % builder, '#'),
356 ('Build Number %s' % step, '#'),
357 ('Slave %s' % result['slave'], '#')
358 ]
359 return result
360
361
362 class MainPage(webapp2.RequestHandler):
363 """Parses a buildlog page."""
364 @render('main.html')
365 @expect_request('url')
agable 2013/04/15 19:33:30 Having a *required* url parameter is kinda weird.
Ryan Tseng 2013/04/17 22:53:48 Done. MainPage now just parses the url and redire
366 def get(self, url):
agable 2013/04/15 19:33:30 I'd reorder the steps this method performs for bet
Ryan Tseng 2013/04/17 22:53:48 Refactored to just do #1. The rest has also been
367 if not url:
368 return {}
369
370 # Redirect the page if we detect a different type of URL.
371 sch, netloc, path, _, _, _ = urlparse.urlparse(url)
372 logging.info(path)
373 if re.match(r'^/((p/)?)(.*)/builders/(.*)/builds/(\d+)$', path):
374 self.redirect('/buildbot/step?url=%s' % url)
375 return {}
376
377 buildlog_query = BuildLogModel.all().filter('url =', url)
378 buildlog = buildlog_query.get()
agable 2013/04/15 19:33:30 377 and 378 can be one line.
Ryan Tseng 2013/04/17 22:53:48 Done.
379 log_fetch_start = time.time()
380 if buildlog:
381 s = zlib.decompress(buildlog.data)
382 else:
383 s = urlfetch.fetch(url, method=urlfetch.GET, deadline=60).content
384 log_fetch_time = time.time() - log_fetch_start
385 all_output = re.findall(r'<span class="(header|stdout)">(.*?)</span>',
agable 2013/04/15 19:33:30 Don't bother performing this regex unless the cach
Ryan Tseng 2013/04/17 22:53:48 Done.
386 s, re.S)
387
388 cached_result = BuildLogResultModel.all().filter(
389 'url =', url).filter('version =', VERSION_ID).get()
390 parse_time_start = time.time()
391 if cached_result:
392 result_output = json.loads(zlib.decompress(cached_result.data))
393 else:
394 result_output = []
395 current_source = None
396 current_string = ''
397 for source, output in all_output:
398 if source == current_source:
399 current_string += output
400 continue
401 else:
402 # We hit a new source, we want to emit whatever we had left and
403 # start anew.
404 if current_string:
405 result_output.append(emit(current_source, current_string))
406 current_string = output
407 current_source = source
408 if current_string:
409 result_output.append(emit(current_source, current_string))
410 compressed_result = zlib.compress(json.dumps(result_output))
411 if len(compressed_result) < 1000 * 1000:
agable 2013/04/15 19:33:30 Use 10**6
Ryan Tseng 2013/04/17 22:53:48 Done.
412 cached_result = BuildLogResultModel(
413 url=url, version=VERSION_ID, data=compressed_result)
414 cached_result.put()
415
416 url_re = r'/[p]/([\w.]+)/builders/(\w+)/builds/(\w+)/steps/(\w+)/logs/.*'
417 master_name, builder_name, build_number, step = re.search(
418 url_re, url).groups()
419
420 ret_code_m = re.search('program finished with exit code (-?\d+)', s)
421 if ret_code_m:
422 ret_code = int(ret_code_m.group(1))
423 if ret_code == 0:
424 status = 'OK'
425 else:
426 status = 'ERROR'
427 else:
428 status = 'RUNNING'
429 ret_code = None
430
431 if ret_code is not None and not buildlog:
432 # Cache this build log if not already.
433 compressed_data = zlib.compress(s)
434 if len(compressed_data) < 1000 * 1000:
435 buildlog = BuildLogModel(url=url, data=compressed_data)
436 buildlog.put()
437 parse_time = time.time() - parse_time_start
438
439 return {
440 'output': result_output,
441 'url': url,
442 'name': step,
443 'breadcrumbs': [
444 ('Master %s' % master_name,
445 'http://build.chromium.org/p/%s/waterfall' % master_name),
446 ('Builder %s' % builder_name,
447 'http://build.chromium.org/p/%s/builders/%s' %
448 (master_name, builder_name)),
449 ('Build Number %s ' % build_number,
450 'http://build.chromium.org/p/%s/builders/%s/builds/%s' %
451 (master_name, builder_name, build_number)),
452 ('Step %s' % step, url)
453 ],
454 'status': status,
455 'ret_code': ret_code,
456 'log_fetch_time': log_fetch_time,
457 'parse_time': parse_time,
458 'compressed_size': len(buildlog.data) if buildlog else -1,
459 'compressed_report': len(cached_result.data) if cached_result else -1,
460 'url': url,
461 'debug': self.request.get('debug'),
462 'size': len(s)
463 }
agable 2013/04/15 19:33:30 Could cache the compressed version of this whole j
Ryan Tseng 2013/04/17 22:53:48 done :) (That's what line 388/412 is) Well, it cac
464
465
466 def webapp_add_wsgi_middleware(app):
467 from google.appengine.ext.appstats import recording
468 app = recording.appstats_wsgi_middleware(app)
469 return app
470
471
472 app = webapp2.WSGIApplication([
473 ('/buildbot/', MainPage),
474 ('/buildbot/step/?', BuildStep),
agable 2013/04/15 19:33:30 See comments on MainPage and BuildStep get methods
Ryan Tseng 2013/04/17 22:53:48 Done.
475 ], debug=True)
476 app = webapp_add_wsgi_middleware(app)
OLDNEW
« no previous file with comments | « app.yaml ('k') | static/css/bootstrap.css » ('j') | static/css/bootstrap-responsive.css » ('J')

Powered by Google App Engine
This is Rietveld 408576698