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

Side by Side Diff: third_party/buildbot_7_12/buildbot/status/web/waterfall.py

Issue 12207158: Bye bye buildbot 0.7.12. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/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
OLDNEW
(Empty)
1 # -*- test-case-name: buildbot.test.test_web -*-
2
3 from zope.interface import implements
4 from twisted.python import log, components
5 from twisted.web import html
6 import urllib
7
8 import time
9 import operator
10
11 from buildbot import interfaces, util
12 from buildbot import version
13 from buildbot.status import builder
14
15 from buildbot.status.web.base import Box, HtmlResource, IBox, ICurrentBox, \
16 ITopBox, td, build_get_class, path_to_build, path_to_step, map_branches
17
18
19
20 class CurrentBox(components.Adapter):
21 # this provides the "current activity" box, just above the builder name
22 implements(ICurrentBox)
23
24 def formatETA(self, prefix, eta):
25 if eta is None:
26 return []
27 if eta < 60:
28 return ["< 1 min"]
29 eta_parts = ["~"]
30 eta_secs = eta
31 if eta_secs > 3600:
32 eta_parts.append("%d hrs" % (eta_secs / 3600))
33 eta_secs %= 3600
34 if eta_secs > 60:
35 eta_parts.append("%d mins" % (eta_secs / 60))
36 eta_secs %= 60
37 abstime = time.strftime("%H:%M", time.localtime(util.now()+eta))
38 return [prefix, " ".join(eta_parts), "at %s" % abstime]
39
40 def getBox(self, status):
41 # getState() returns offline, idle, or building
42 state, builds = self.original.getState()
43
44 # look for upcoming builds. We say the state is "waiting" if the
45 # builder is otherwise idle and there is a scheduler which tells us a
46 # build will be performed some time in the near future. TODO: this
47 # functionality used to be in BuilderStatus.. maybe this code should
48 # be merged back into it.
49 upcoming = []
50 builderName = self.original.getName()
51 for s in status.getSchedulers():
52 if builderName in s.listBuilderNames():
53 upcoming.extend(s.getPendingBuildTimes())
54 if state == "idle" and upcoming:
55 state = "waiting"
56
57 if state == "building":
58 text = ["building"]
59 if builds:
60 for b in builds:
61 eta = b.getETA()
62 text.extend(self.formatETA("ETA in", eta))
63 elif state == "offline":
64 text = ["offline"]
65 elif state == "idle":
66 text = ["idle"]
67 elif state == "waiting":
68 text = ["waiting"]
69 else:
70 # just in case I add a state and forget to update this
71 text = [state]
72
73 # TODO: for now, this pending/upcoming stuff is in the "current
74 # activity" box, but really it should go into a "next activity" row
75 # instead. The only times it should show up in "current activity" is
76 # when the builder is otherwise idle.
77
78 # are any builds pending? (waiting for a slave to be free)
79 pbs = self.original.getPendingBuilds()
80 if pbs:
81 text.append("%d pending" % len(pbs))
82 for t in upcoming:
83 eta = t - util.now()
84 text.extend(self.formatETA("next in", eta))
85 return Box(text, class_="Activity " + state)
86
87 components.registerAdapter(CurrentBox, builder.BuilderStatus, ICurrentBox)
88
89
90 class BuildTopBox(components.Adapter):
91 # this provides a per-builder box at the very top of the display,
92 # showing the results of the most recent build
93 implements(IBox)
94
95 def getBox(self, req):
96 assert interfaces.IBuilderStatus(self.original)
97 branches = [b for b in req.args.get("branch", []) if b]
98 builder = self.original
99 builds = list(builder.generateFinishedBuilds(map_branches(branches),
100 num_builds=1))
101 if not builds:
102 return Box(["none"], class_="LastBuild")
103 b = builds[0]
104 name = b.getBuilder().getName()
105 number = b.getNumber()
106 url = path_to_build(req, b)
107 text = b.getText()
108 tests_failed = b.getSummaryStatistic('tests-failed', operator.add, 0)
109 if tests_failed: text.extend(["Failed tests: %d" % tests_failed])
110 # TODO: maybe add logs?
111 # TODO: add link to the per-build page at 'url'
112 class_ = build_get_class(b)
113 return Box(text, class_="LastBuild %s" % class_)
114 components.registerAdapter(BuildTopBox, builder.BuilderStatus, ITopBox)
115
116 class BuildBox(components.Adapter):
117 # this provides the yellow "starting line" box for each build
118 implements(IBox)
119
120 def getBox(self, req):
121 b = self.original
122 number = b.getNumber()
123 url = path_to_build(req, b)
124 reason = b.getReason()
125 text = ('<a title="Reason: %s" href="%s">Build %d</a>'
126 % (html.escape(reason), url, number))
127 class_ = "start"
128 if b.isFinished() and not b.getSteps():
129 # the steps have been pruned, so there won't be any indication
130 # of whether it succeeded or failed.
131 class_ = build_get_class(b)
132 return Box([text], class_="BuildStep " + class_)
133 components.registerAdapter(BuildBox, builder.BuildStatus, IBox)
134
135 class StepBox(components.Adapter):
136 implements(IBox)
137
138 def getBox(self, req):
139 urlbase = path_to_step(req, self.original)
140 text = self.original.getText()
141 if text is None:
142 log.msg("getText() gave None", urlbase)
143 text = []
144 text = text[:]
145 logs = self.original.getLogs()
146 for num in range(len(logs)):
147 name = logs[num].getName()
148 if logs[num].hasContents():
149 url = urlbase + "/logs/%s" % urllib.quote(name)
150 text.append("<a href=\"%s\">%s</a>" % (url, html.escape(name)))
151 else:
152 text.append(html.escape(name))
153 urls = self.original.getURLs()
154 ex_url_class = "BuildStep external"
155 for name, target in urls.items():
156 text.append('[<a href="%s" class="%s">%s</a>]' %
157 (target, ex_url_class, html.escape(name)))
158 class_ = "BuildStep " + build_get_class(self.original)
159 return Box(text, class_=class_)
160 components.registerAdapter(StepBox, builder.BuildStepStatus, IBox)
161
162
163 class EventBox(components.Adapter):
164 implements(IBox)
165
166 def getBox(self, req):
167 text = self.original.getText()
168 class_ = "Event"
169 return Box(text, class_=class_)
170 components.registerAdapter(EventBox, builder.Event, IBox)
171
172
173 class Spacer:
174 implements(interfaces.IStatusEvent)
175
176 def __init__(self, start, finish):
177 self.started = start
178 self.finished = finish
179
180 def getTimes(self):
181 return (self.started, self.finished)
182 def getText(self):
183 return []
184
185 class SpacerBox(components.Adapter):
186 implements(IBox)
187
188 def getBox(self, req):
189 #b = Box(["spacer"], "white")
190 b = Box([])
191 b.spacer = True
192 return b
193 components.registerAdapter(SpacerBox, Spacer, IBox)
194
195 def insertGaps(g, showEvents, lastEventTime, idleGap=2):
196 debug = False
197
198 e = g.next()
199 starts, finishes = e.getTimes()
200 if debug: log.msg("E0", starts, finishes)
201 if finishes == 0:
202 finishes = starts
203 if debug: log.msg("E1 finishes=%s, gap=%s, lET=%s" % \
204 (finishes, idleGap, lastEventTime))
205 if finishes is not None and finishes + idleGap < lastEventTime:
206 if debug: log.msg(" spacer0")
207 yield Spacer(finishes, lastEventTime)
208
209 followingEventStarts = starts
210 if debug: log.msg(" fES0", starts)
211 yield e
212
213 while 1:
214 e = g.next()
215 if not showEvents and isinstance(e, builder.Event):
216 continue
217 starts, finishes = e.getTimes()
218 if debug: log.msg("E2", starts, finishes)
219 if finishes == 0:
220 finishes = starts
221 if finishes is not None and finishes + idleGap < followingEventStarts:
222 # there is a gap between the end of this event and the beginning
223 # of the next one. Insert an idle event so the waterfall display
224 # shows a gap here.
225 if debug:
226 log.msg(" finishes=%s, gap=%s, fES=%s" % \
227 (finishes, idleGap, followingEventStarts))
228 yield Spacer(finishes, followingEventStarts)
229 yield e
230 followingEventStarts = starts
231 if debug: log.msg(" fES1", starts)
232
233 HELP = '''
234 <form action="../waterfall" method="GET">
235
236 <h1>The Waterfall Display</h1>
237
238 <p>The Waterfall display can be controlled by adding query arguments to the
239 URL. For example, if your Waterfall is accessed via the URL
240 <tt>http://buildbot.example.org:8080</tt>, then you could add a
241 <tt>branch=</tt> argument (described below) by going to
242 <tt>http://buildbot.example.org:8080?branch=beta4</tt> instead. Remember that
243 query arguments are separated from each other with ampersands, but they are
244 separated from the main URL with a question mark, so to add a
245 <tt>branch=</tt> and two <tt>builder=</tt> arguments, you would use
246 <tt>http://buildbot.example.org:8080?branch=beta4&amp;builder=unix&amp;builder=m acos</tt>.</p>
247
248 <h2>Limiting the Displayed Interval</h2>
249
250 <p>The <tt>last_time=</tt> argument is a unix timestamp (seconds since the
251 start of 1970) that will be used as an upper bound on the interval of events
252 displayed: nothing will be shown that is more recent than the given time.
253 When no argument is provided, all events up to and including the most recent
254 steps are included.</p>
255
256 <p>The <tt>first_time=</tt> argument provides the lower bound. No events will
257 be displayed that occurred <b>before</b> this timestamp. Instead of providing
258 <tt>first_time=</tt>, you can provide <tt>show_time=</tt>: in this case,
259 <tt>first_time</tt> will be set equal to <tt>last_time</tt> minus
260 <tt>show_time</tt>. <tt>show_time</tt> overrides <tt>first_time</tt>.</p>
261
262 <p>The display normally shows the latest 200 events that occurred in the
263 given interval, where each timestamp on the left hand edge counts as a single
264 event. You can add a <tt>num_events=</tt> argument to override this this.</p>
265
266 <h2>Showing non-Build events</h2>
267
268 <p>By passing <tt>show_events=true</tt>, you can add the "buildslave
269 attached", "buildslave detached", and "builder reconfigured" events that
270 appear in-between the actual builds.</p>
271
272 %(show_events_input)s
273
274 <h2>Showing only the Builders with failures</h2>
275
276 <p>By adding the <tt>failures_only=true</tt> argument, the display will be limit ed
277 to showing builders that are currently failing. A builder is considered
278 failing if the last finished build was not successful, a step in the current
279 build(s) failed, or if the builder is offline.
280
281 %(failures_only_input)s
282
283 <h2>Showing only Certain Branches</h2>
284
285 <p>If you provide one or more <tt>branch=</tt> arguments, the display will be
286 limited to builds that used one of the given branches. If no <tt>branch=</tt>
287 arguments are given, builds from all branches will be displayed.</p>
288
289 Erase the text from these "Show Branch:" boxes to remove that branch filter.
290
291 %(show_branches_input)s
292
293 <h2>Limiting the Builders that are Displayed</h2>
294
295 <p>By adding one or more <tt>builder=</tt> arguments, the display will be
296 limited to showing builds that ran on the given builders. This serves to
297 limit the display to the specific named columns. If no <tt>builder=</tt>
298 arguments are provided, all Builders will be displayed.</p>
299
300 <p>To view a Waterfall page with only a subset of Builders displayed, select
301 the Builders you are interested in here.</p>
302
303 %(show_builders_input)s
304
305 <h2>Limiting the Builds that are Displayed</h2>
306
307 <p>By adding one or more <tt>committer=</tt> arguments, the display will be
308 limited to showing builds that were started by the given committer. If no
309 <tt>committer=</tt> arguments are provided, all builds will be displayed.</p>
310
311 <p>To view a Waterfall page with only a subset of Builds displayed, select
312 the committers your are interested in here.</p>
313
314 %(show_committers_input)s
315
316
317 <h2>Auto-reloading the Page</h2>
318
319 <p>Adding a <tt>reload=</tt> argument will cause the page to automatically
320 reload itself after that many seconds.</p>
321
322 %(show_reload_input)s
323
324 <h2>Reload Waterfall Page</h2>
325
326 <input type="submit" value="View Waterfall" />
327 </form>
328 '''
329
330 class WaterfallHelp(HtmlResource):
331 title = "Waterfall Help"
332
333 def __init__(self, categories=None):
334 HtmlResource.__init__(self)
335 self.categories = categories
336
337 def body(self, request):
338 data = ''
339 status = self.getStatus(request)
340
341 showEvents_checked = ''
342 if request.args.get("show_events", ["false"])[0].lower() == "true":
343 showEvents_checked = 'checked="checked"'
344 show_events_input = ('<p>'
345 '<input type="checkbox" name="show_events" '
346 'value="true" %s>'
347 'Show non-Build events'
348 '</p>\n'
349 ) % showEvents_checked
350
351 failuresOnly_checked = ''
352 if request.args.get("failures_only", ["false"])[0].lower() == "true":
353 failuresOnly_checked = 'checked="checked"'
354 failures_only_input = ('<p>'
355 '<input type="checkbox" name="failures_only" '
356 'value="true" %s>'
357 'Show failures only'
358 '</p>\n'
359 ) % failuresOnly_checked
360
361 branches = [b
362 for b in request.args.get("branch", [])
363 if b]
364 branches.append('')
365 show_branches_input = '<table>\n'
366 for b in branches:
367 show_branches_input += ('<tr>'
368 '<td>Show Branch: '
369 '<input type="text" name="branch" '
370 'value="%s">'
371 '</td></tr>\n'
372 ) % (html.escape(b),)
373 show_branches_input += '</table>\n'
374
375 # this has a set of toggle-buttons to let the user choose the
376 # builders
377 showBuilders = request.args.get("show", [])
378 showBuilders.extend(request.args.get("builder", []))
379 allBuilders = status.getBuilderNames(categories=self.categories)
380
381 show_builders_input = '<table>\n'
382 for bn in allBuilders:
383 checked = ""
384 if bn in showBuilders:
385 checked = 'checked="checked"'
386 show_builders_input += ('<tr>'
387 '<td><input type="checkbox"'
388 ' name="builder" '
389 'value="%s" %s></td> '
390 '<td>%s</td></tr>\n'
391 ) % (bn, checked, bn)
392 show_builders_input += '</table>\n'
393
394 committers = [c for c in request.args.get("committer", []) if c]
395 committers.append('')
396 show_committers_input = '<table>\n'
397 for c in committers:
398 show_committers_input += ('<tr>'
399 '<td>Show committer: '
400 '<input type="text" name="committer" '
401 'value="%s">'
402 '</td></tr>\n'
403 ) % (html.escape(c),)
404 show_committers_input += '</table>\n'
405
406 # a couple of radio-button selectors for refresh time will appear
407 # just after that text
408 show_reload_input = '<table>\n'
409 times = [("none", "None"),
410 ("60", "60 seconds"),
411 ("300", "5 minutes"),
412 ("600", "10 minutes"),
413 ]
414 current_reload_time = request.args.get("reload", ["none"])
415 if current_reload_time:
416 current_reload_time = current_reload_time[0]
417 if current_reload_time not in [t[0] for t in times]:
418 times.insert(0, (current_reload_time, current_reload_time) )
419 for value, name in times:
420 checked = ""
421 if value == current_reload_time:
422 checked = 'checked="checked"'
423 show_reload_input += ('<tr>'
424 '<td><input type="radio" name="reload" '
425 'value="%s" %s></td> '
426 '<td>%s</td></tr>\n'
427 ) % (html.escape(value), checked, html.escape( name))
428 show_reload_input += '</table>\n'
429
430 fields = {"show_events_input": show_events_input,
431 "show_branches_input": show_branches_input,
432 "show_builders_input": show_builders_input,
433 "show_committers_input": show_committers_input,
434 "show_reload_input": show_reload_input,
435 "failures_only_input": failures_only_input,
436 }
437 data += HELP % fields
438 return data
439
440 class WaterfallStatusResource(HtmlResource):
441 """This builds the main status page, with the waterfall display, and
442 all child pages."""
443
444 def __init__(self, categories=None, num_events=200, num_events_max=None):
445 HtmlResource.__init__(self)
446 self.categories = categories
447 self.num_events=num_events
448 self.num_events_max=num_events_max
449 self.putChild("help", WaterfallHelp(categories))
450
451 def getTitle(self, request):
452 status = self.getStatus(request)
453 p = status.getProjectName()
454 if p:
455 return "BuildBot: %s" % p
456 else:
457 return "BuildBot"
458
459 def getChangemaster(self, request):
460 # TODO: this wants to go away, access it through IStatus
461 return request.site.buildbot_service.getChangeSvc()
462
463 def get_reload_time(self, request):
464 if "reload" in request.args:
465 try:
466 reload_time = int(request.args["reload"][0])
467 return max(reload_time, 15)
468 except ValueError:
469 pass
470 return None
471
472 def head(self, request):
473 head = ''
474 reload_time = self.get_reload_time(request)
475 if reload_time is not None:
476 head += '<meta http-equiv="refresh" content="%d">\n' % reload_time
477 return head
478
479 def isSuccess(self, builderStatus):
480 # Helper function to return True if the builder is not failing.
481 # The function will return false if the current state is "offline",
482 # the last build was not successful, or if a step from the current
483 # build(s) failed.
484
485 # Make sure the builder is online.
486 if builderStatus.getState()[0] == 'offline':
487 return False
488
489 # Look at the last finished build to see if it was success or not.
490 lastBuild = builderStatus.getLastFinishedBuild()
491 if lastBuild and lastBuild.getResults() != builder.SUCCESS:
492 return False
493
494 # Check all the current builds to see if one step is already
495 # failing.
496 currentBuilds = builderStatus.getCurrentBuilds()
497 if currentBuilds:
498 for build in currentBuilds:
499 for step in build.getSteps():
500 if step.getResults()[0] == builder.FAILURE:
501 return False
502
503 # The last finished build was successful, and all the current builds
504 # don't have any failed steps.
505 return True
506
507 def body(self, request):
508 "This method builds the main waterfall display."
509
510 status = self.getStatus(request)
511 data = ''
512
513 projectName = status.getProjectName()
514 projectURL = status.getProjectURL()
515
516 phase = request.args.get("phase",["2"])
517 phase = int(phase[0])
518
519 # we start with all Builders available to this Waterfall: this is
520 # limited by the config-file -time categories= argument, and defaults
521 # to all defined Builders.
522 allBuilderNames = status.getBuilderNames(categories=self.categories)
523 builders = [status.getBuilder(name) for name in allBuilderNames]
524
525 # but if the URL has one or more builder= arguments (or the old show=
526 # argument, which is still accepted for backwards compatibility), we
527 # use that set of builders instead. We still don't show anything
528 # outside the config-file time set limited by categories=.
529 showBuilders = request.args.get("show", [])
530 showBuilders.extend(request.args.get("builder", []))
531 if showBuilders:
532 builders = [b for b in builders if b.name in showBuilders]
533
534 # now, if the URL has one or category= arguments, use them as a
535 # filter: only show those builders which belong to one of the given
536 # categories.
537 showCategories = request.args.get("category", [])
538 if showCategories:
539 builders = [b for b in builders if b.category in showCategories]
540
541 # If the URL has the failures_only=true argument, we remove all the
542 # builders that are not currently red or won't be turning red at the end
543 # of their current run.
544 failuresOnly = request.args.get("failures_only", ["false"])[0]
545 if failuresOnly.lower() == "true":
546 builders = [b for b in builders if not self.isSuccess(b)]
547
548 builderNames = [b.name for b in builders]
549
550 if phase == -1:
551 return self.body0(request, builders)
552 (changeNames, builderNames, timestamps, eventGrid, sourceEvents) = \
553 self.buildGrid(request, builders)
554 if phase == 0:
555 return self.phase0(request, (changeNames + builderNames),
556 timestamps, eventGrid)
557 # start the table: top-header material
558 data += '<table border="0" cellspacing="0">\n'
559
560 if projectName and projectURL:
561 # TODO: this is going to look really ugly
562 topleft = '<a href="%s">%s</a><br />last build' % \
563 (projectURL, projectName)
564 else:
565 topleft = "last build"
566 data += ' <tr class="LastBuild">\n'
567 data += td(topleft, align="right", colspan=2, class_="Project")
568 for b in builders:
569 box = ITopBox(b).getBox(request)
570 data += box.td(align="center")
571 data += " </tr>\n"
572
573 data += ' <tr class="Activity">\n'
574 data += td('current activity', align='right', colspan=2)
575 for b in builders:
576 box = ICurrentBox(b).getBox(status)
577 data += box.td(align="center")
578 data += " </tr>\n"
579
580 data += " <tr>\n"
581 TZ = time.tzname[time.localtime()[-1]]
582 data += td("time (%s)" % TZ, align="center", class_="Time")
583 data += td('<a href="%s">changes</a>' % request.childLink("../changes"),
584 align="center", class_="Change")
585 for name in builderNames:
586 safename = urllib.quote(name, safe='')
587 data += td('<a href="%s">%s</a>' %
588 (request.childLink("../builders/%s" % safename), name),
589 align="center", class_="Builder")
590 data += " </tr>\n"
591
592 if phase == 1:
593 f = self.phase1
594 else:
595 f = self.phase2
596 data += f(request, changeNames + builderNames, timestamps, eventGrid,
597 sourceEvents)
598
599 data += "</table>\n"
600
601
602 def with_args(req, remove_args=[], new_args=[], new_path=None):
603 # sigh, nevow makes this sort of manipulation easier
604 newargs = req.args.copy()
605 for argname in remove_args:
606 newargs[argname] = []
607 if "branch" in newargs:
608 newargs["branch"] = [b for b in newargs["branch"] if b]
609 for k,v in new_args:
610 if k in newargs:
611 newargs[k].append(v)
612 else:
613 newargs[k] = [v]
614 newquery = "&".join(["%s=%s" % (urllib.quote(k), urllib.quote(v))
615 for k in newargs
616 for v in newargs[k]
617 ])
618 if new_path:
619 new_url = new_path
620 elif req.prepath:
621 new_url = req.prepath[-1]
622 else:
623 new_url = ''
624 if newquery:
625 new_url += "?" + newquery
626 return new_url
627
628 if timestamps:
629 bottom = timestamps[-1]
630 nextpage = with_args(request, ["last_time"],
631 [("last_time", str(int(bottom)))])
632 data += '[<a href="%s">next page</a>]\n' % nextpage
633
634 helpurl = self.path_to_root(request) + "waterfall/help"
635 helppage = with_args(request, new_path=helpurl)
636 data += '[<a href="%s">help</a>]\n' % helppage
637
638 if self.get_reload_time(request) is not None:
639 no_reload_page = with_args(request, remove_args=["reload"])
640 data += '[<a href="%s">Stop Reloading</a>]\n' % no_reload_page
641
642 data += "<br />\n"
643 data += self.footer(status, request)
644
645 return data
646
647 def body0(self, request, builders):
648 # build the waterfall display
649 data = ""
650 data += "<h2>Basic display</h2>\n"
651 data += '<p>See <a href="%s">here</a>' % request.childLink("../waterfall ")
652 data += " for the waterfall display</p>\n"
653
654 data += '<table border="0" cellspacing="0">\n'
655 names = map(lambda builder: builder.name, builders)
656
657 # the top row is two blank spaces, then the top-level status boxes
658 data += " <tr>\n"
659 data += td("", colspan=2)
660 for b in builders:
661 text = ""
662 state, builds = b.getState()
663 if state != "offline":
664 text += "%s<br />\n" % state #b.getCurrentBig().text[0]
665 else:
666 text += "OFFLINE<br />\n"
667 data += td(text, align="center")
668
669 # the next row has the column headers: time, changes, builder names
670 data += " <tr>\n"
671 data += td("Time", align="center")
672 data += td("Changes", align="center")
673 for name in names:
674 data += td('<a href="%s">%s</a>' %
675 (request.childLink("../" + urllib.quote(name)), name),
676 align="center")
677 data += " </tr>\n"
678
679 # all further rows involve timestamps, commit events, and build events
680 data += " <tr>\n"
681 data += td("04:00", align="bottom")
682 data += td("fred", align="center")
683 for name in names:
684 data += td("stuff", align="center")
685 data += " </tr>\n"
686
687 data += "</table>\n"
688 return data
689
690 def buildGrid(self, request, builders):
691 debug = False
692 # TODO: see if we can use a cached copy
693
694 showEvents = False
695 if request.args.get("show_events", ["false"])[0].lower() == "true":
696 showEvents = True
697 filterCategories = request.args.get('category', [])
698 filterBranches = [b for b in request.args.get("branch", []) if b]
699 filterBranches = map_branches(filterBranches)
700 filterCommitters = [c for c in request.args.get("committer", []) if c]
701 maxTime = int(request.args.get("last_time", [util.now()])[0])
702 if "show_time" in request.args:
703 minTime = maxTime - int(request.args["show_time"][0])
704 elif "first_time" in request.args:
705 minTime = int(request.args["first_time"][0])
706 elif filterBranches or filterCommitters:
707 minTime = util.now() - 24 * 60 * 60
708 else:
709 minTime = 0
710 spanLength = 10 # ten-second chunks
711 req_events=int(request.args.get("num_events", [self.num_events])[0])
712 if self.num_events_max and req_events > self.num_events_max:
713 maxPageLen = self.num_events_max
714 else:
715 maxPageLen = req_events
716
717 # first step is to walk backwards in time, asking each column
718 # (commit, all builders) if they have any events there. Build up the
719 # array of events, and stop when we have a reasonable number.
720
721 commit_source = self.getChangemaster(request)
722
723 lastEventTime = util.now()
724 sources = [commit_source] + builders
725 changeNames = ["changes"]
726 builderNames = map(lambda builder: builder.getName(), builders)
727 sourceNames = changeNames + builderNames
728 sourceEvents = []
729 sourceGenerators = []
730
731 def get_event_from(g):
732 try:
733 while True:
734 e = g.next()
735 # e might be builder.BuildStepStatus,
736 # builder.BuildStatus, builder.Event,
737 # waterfall.Spacer(builder.Event), or changes.Change .
738 # The showEvents=False flag means we should hide
739 # builder.Event .
740 if not showEvents and isinstance(e, builder.Event):
741 continue
742 break
743 event = interfaces.IStatusEvent(e)
744 if debug:
745 log.msg("gen %s gave1 %s" % (g, event.getText()))
746 except StopIteration:
747 event = None
748 return event
749
750 for s in sources:
751 gen = insertGaps(s.eventGenerator(filterBranches,
752 filterCategories,
753 filterCommitters,
754 minTime),
755 showEvents,
756 lastEventTime)
757 sourceGenerators.append(gen)
758 # get the first event
759 sourceEvents.append(get_event_from(gen))
760 eventGrid = []
761 timestamps = []
762
763 lastEventTime = 0
764 for e in sourceEvents:
765 if e and e.getTimes()[0] > lastEventTime:
766 lastEventTime = e.getTimes()[0]
767 if lastEventTime == 0:
768 lastEventTime = util.now()
769
770 spanStart = lastEventTime - spanLength
771 debugGather = 0
772
773 while 1:
774 if debugGather: log.msg("checking (%s,]" % spanStart)
775 # the tableau of potential events is in sourceEvents[]. The
776 # window crawls backwards, and we examine one source at a time.
777 # If the source's top-most event is in the window, is it pushed
778 # onto the events[] array and the tableau is refilled. This
779 # continues until the tableau event is not in the window (or is
780 # missing).
781
782 spanEvents = [] # for all sources, in this span. row of eventGrid
783 firstTimestamp = None # timestamp of first event in the span
784 lastTimestamp = None # last pre-span event, for next span
785
786 for c in range(len(sourceGenerators)):
787 events = [] # for this source, in this span. cell of eventGrid
788 event = sourceEvents[c]
789 while event and spanStart < event.getTimes()[0]:
790 # to look at windows that don't end with the present,
791 # condition the .append on event.time <= spanFinish
792 if not IBox(event, None):
793 log.msg("BAD EVENT", event, event.getText())
794 assert 0
795 if debug:
796 log.msg("pushing", event.getText(), event)
797 events.append(event)
798 starts, finishes = event.getTimes()
799 firstTimestamp = util.earlier(firstTimestamp, starts)
800 event = get_event_from(sourceGenerators[c])
801 if debug:
802 log.msg("finished span")
803
804 if event:
805 # this is the last pre-span event for this source
806 lastTimestamp = util.later(lastTimestamp,
807 event.getTimes()[0])
808 if debugGather:
809 log.msg(" got %s from %s" % (events, sourceNames[c]))
810 sourceEvents[c] = event # refill the tableau
811 spanEvents.append(events)
812
813 # only show events older than maxTime. This makes it possible to
814 # visit a page that shows what it would be like to scroll off the
815 # bottom of this one.
816 if firstTimestamp is not None and firstTimestamp <= maxTime:
817 eventGrid.append(spanEvents)
818 timestamps.append(firstTimestamp)
819
820 if lastTimestamp:
821 spanStart = lastTimestamp - spanLength
822 else:
823 # no more events
824 break
825 if minTime is not None and lastTimestamp < minTime:
826 break
827
828 if len(timestamps) > maxPageLen:
829 break
830
831
832 # now loop
833
834 # loop is finished. now we have eventGrid[] and timestamps[]
835 if debugGather: log.msg("finished loop")
836 assert(len(timestamps) == len(eventGrid))
837 return (changeNames, builderNames, timestamps, eventGrid, sourceEvents)
838
839 def phase0(self, request, sourceNames, timestamps, eventGrid):
840 # phase0 rendering
841 if not timestamps:
842 return "no events"
843 data = ""
844 for r in range(0, len(timestamps)):
845 data += "<p>\n"
846 data += "[%s]<br />" % timestamps[r]
847 row = eventGrid[r]
848 assert(len(row) == len(sourceNames))
849 for c in range(0, len(row)):
850 if row[c]:
851 data += "<b>%s</b><br />\n" % sourceNames[c]
852 for e in row[c]:
853 log.msg("Event", r, c, sourceNames[c], e.getText())
854 lognames = [loog.getName() for loog in e.getLogs()]
855 data += "%s: %s: %s<br />" % (e.getText(),
856 e.getTimes()[0],
857 lognames)
858 else:
859 data += "<b>%s</b> [none]<br />\n" % sourceNames[c]
860 return data
861
862 def phase1(self, request, sourceNames, timestamps, eventGrid,
863 sourceEvents):
864 # phase1 rendering: table, but boxes do not overlap
865 data = ""
866 if not timestamps:
867 return data
868 lastDate = None
869 for r in range(0, len(timestamps)):
870 chunkstrip = eventGrid[r]
871 # chunkstrip is a horizontal strip of event blocks. Each block
872 # is a vertical list of events, all for the same source.
873 assert(len(chunkstrip) == len(sourceNames))
874 maxRows = reduce(lambda x,y: max(x,y),
875 map(lambda x: len(x), chunkstrip))
876 for i in range(maxRows):
877 data += " <tr>\n";
878 if i == 0:
879 stuff = []
880 # add the date at the beginning, and each time it changes
881 today = time.strftime("<b>%d %b %Y</b>",
882 time.localtime(timestamps[r]))
883 todayday = time.strftime("<b>%a</b>",
884 time.localtime(timestamps[r]))
885 if today != lastDate:
886 stuff.append(todayday)
887 stuff.append(today)
888 lastDate = today
889 stuff.append(
890 time.strftime("%H:%M:%S",
891 time.localtime(timestamps[r])))
892 data += td(stuff, valign="bottom", align="center",
893 rowspan=maxRows, class_="Time")
894 for c in range(0, len(chunkstrip)):
895 block = chunkstrip[c]
896 assert(block != None) # should be [] instead
897 # bottom-justify
898 offset = maxRows - len(block)
899 if i < offset:
900 data += td("")
901 else:
902 e = block[i-offset]
903 box = IBox(e).getBox(request)
904 box.parms["show_idle"] = 1
905 data += box.td(valign="top", align="center")
906 data += " </tr>\n"
907
908 return data
909
910 def phase2(self, request, sourceNames, timestamps, eventGrid,
911 sourceEvents):
912 data = ""
913 if not timestamps:
914 return data
915 # first pass: figure out the height of the chunks, populate grid
916 grid = []
917 for i in range(1+len(sourceNames)):
918 grid.append([])
919 # grid is a list of columns, one for the timestamps, and one per
920 # event source. Each column is exactly the same height. Each element
921 # of the list is a single <td> box.
922 lastDate = time.strftime("<b>%d %b %Y</b>",
923 time.localtime(util.now()))
924 for r in range(0, len(timestamps)):
925 chunkstrip = eventGrid[r]
926 # chunkstrip is a horizontal strip of event blocks. Each block
927 # is a vertical list of events, all for the same source.
928 assert(len(chunkstrip) == len(sourceNames))
929 maxRows = reduce(lambda x,y: max(x,y),
930 map(lambda x: len(x), chunkstrip))
931 for i in range(maxRows):
932 if i != maxRows-1:
933 grid[0].append(None)
934 else:
935 # timestamp goes at the bottom of the chunk
936 stuff = []
937 # add the date at the beginning (if it is not the same as
938 # today's date), and each time it changes
939 todayday = time.strftime("<b>%a</b>",
940 time.localtime(timestamps[r]))
941 today = time.strftime("<b>%d %b %Y</b>",
942 time.localtime(timestamps[r]))
943 if today != lastDate:
944 stuff.append(todayday)
945 stuff.append(today)
946 lastDate = today
947 stuff.append(
948 time.strftime("%H:%M:%S",
949 time.localtime(timestamps[r])))
950 grid[0].append(Box(text=stuff, class_="Time",
951 valign="bottom", align="center"))
952
953 # at this point the timestamp column has been populated with
954 # maxRows boxes, most None but the last one has the time string
955 for c in range(0, len(chunkstrip)):
956 block = chunkstrip[c]
957 assert(block != None) # should be [] instead
958 for i in range(maxRows - len(block)):
959 # fill top of chunk with blank space
960 grid[c+1].append(None)
961 for i in range(len(block)):
962 # so the events are bottom-justified
963 b = IBox(block[i]).getBox(request)
964 b.parms['valign'] = "top"
965 b.parms['align'] = "center"
966 grid[c+1].append(b)
967 # now all the other columns have maxRows new boxes too
968 # populate the last row, if empty
969 gridlen = len(grid[0])
970 for i in range(len(grid)):
971 strip = grid[i]
972 assert(len(strip) == gridlen)
973 if strip[-1] == None:
974 if sourceEvents[i-1]:
975 filler = IBox(sourceEvents[i-1]).getBox(request)
976 else:
977 # this can happen if you delete part of the build history
978 filler = Box(text=["?"], align="center")
979 strip[-1] = filler
980 strip[-1].parms['rowspan'] = 1
981 # second pass: bubble the events upwards to un-occupied locations
982 # Every square of the grid that has a None in it needs to have
983 # something else take its place.
984 noBubble = request.args.get("nobubble",['0'])
985 noBubble = int(noBubble[0])
986 if not noBubble:
987 for col in range(len(grid)):
988 strip = grid[col]
989 if col == 1: # changes are handled differently
990 for i in range(2, len(strip)+1):
991 # only merge empty boxes. Don't bubble commit boxes.
992 if strip[-i] == None:
993 next = strip[-i+1]
994 assert(next)
995 if next:
996 #if not next.event:
997 if next.spacer:
998 # bubble the empty box up
999 strip[-i] = next
1000 strip[-i].parms['rowspan'] += 1
1001 strip[-i+1] = None
1002 else:
1003 # we are above a commit box. Leave it
1004 # be, and turn the current box into an
1005 # empty one
1006 strip[-i] = Box([], rowspan=1,
1007 comment="commit bubble")
1008 strip[-i].spacer = True
1009 else:
1010 # we are above another empty box, which
1011 # somehow wasn't already converted.
1012 # Shouldn't happen
1013 pass
1014 else:
1015 for i in range(2, len(strip)+1):
1016 # strip[-i] will go from next-to-last back to first
1017 if strip[-i] == None:
1018 # bubble previous item up
1019 assert(strip[-i+1] != None)
1020 strip[-i] = strip[-i+1]
1021 strip[-i].parms['rowspan'] += 1
1022 strip[-i+1] = None
1023 else:
1024 strip[-i].parms['rowspan'] = 1
1025 # third pass: render the HTML table
1026 for i in range(gridlen):
1027 data += " <tr>\n";
1028 for strip in grid:
1029 b = strip[i]
1030 if b:
1031 # convert data to a unicode string, whacking any non-ASCII c haracters it might contain
1032 s = b.td()
1033 if isinstance(s, unicode):
1034 s = s.encode("utf-8", "replace")
1035 data += s
1036 else:
1037 if noBubble:
1038 data += td([])
1039 # Nones are left empty, rowspan should make it all fit
1040 data += " </tr>\n"
1041 return data
1042
OLDNEW
« no previous file with comments | « third_party/buildbot_7_12/buildbot/status/web/tests.py ('k') | third_party/buildbot_7_12/buildbot/status/web/xmlrpc.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698