(refs #35) fix dependent scheduler re-checking: make calculation of upstream lazier
[buildbot.git] / buildbot / status / web / waterfall.py
blobd8a4e069c301fb03ba8d5ef97106e74ca9fb5489
1 # -*- test-case-name: buildbot.test.test_web -*-
3 from zope.interface import implements
4 from twisted.python import log, components
5 from twisted.web import html
6 import urllib
8 import time
9 import operator
11 from buildbot import interfaces, util
12 from buildbot import version
13 from buildbot.status import builder
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
20 class CurrentBox(components.Adapter):
21 # this provides the "current activity" box, just above the builder name
22 implements(ICurrentBox)
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]
40 def getBox(self, status):
41 # getState() returns offline, idle, or building
42 state, builds = self.original.getState()
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"
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]
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.
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)
87 components.registerAdapter(CurrentBox, builder.BuilderStatus, ICurrentBox)
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)
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)
116 class BuildBox(components.Adapter):
117 # this provides the yellow "starting line" box for each build
118 implements(IBox)
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)
135 class StepBox(components.Adapter):
136 implements(IBox)
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)
163 class EventBox(components.Adapter):
164 implements(IBox)
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)
173 class Spacer:
174 implements(interfaces.IStatusEvent)
176 def __init__(self, start, finish):
177 self.started = start
178 self.finished = finish
180 def getTimes(self):
181 return (self.started, self.finished)
182 def getText(self):
183 return []
185 class SpacerBox(components.Adapter):
186 implements(IBox)
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)
195 def insertGaps(g, lastEventTime, idleGap=2):
196 debug = False
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)
209 followingEventStarts = starts
210 if debug: log.msg(" fES0", starts)
211 yield e
213 while 1:
214 e = g.next()
215 starts, finishes = e.getTimes()
216 if debug: log.msg("E2", starts, finishes)
217 if finishes == 0:
218 finishes = starts
219 if finishes is not None and finishes + idleGap < followingEventStarts:
220 # there is a gap between the end of this event and the beginning
221 # of the next one. Insert an idle event so the waterfall display
222 # shows a gap here.
223 if debug:
224 log.msg(" finishes=%s, gap=%s, fES=%s" % \
225 (finishes, idleGap, followingEventStarts))
226 yield Spacer(finishes, followingEventStarts)
227 yield e
228 followingEventStarts = starts
229 if debug: log.msg(" fES1", starts)
231 HELP = '''
232 <form action="../waterfall" method="GET">
234 <h1>The Waterfall Display</h1>
236 <p>The Waterfall display can be controlled by adding query arguments to the
237 URL. For example, if your Waterfall is accessed via the URL
238 <tt>http://buildbot.example.org:8080</tt>, then you could add a
239 <tt>branch=</tt> argument (described below) by going to
240 <tt>http://buildbot.example.org:8080?branch=beta4</tt> instead. Remember that
241 query arguments are separated from each other with ampersands, but they are
242 separated from the main URL with a question mark, so to add a
243 <tt>branch=</tt> and two <tt>builder=</tt> arguments, you would use
244 <tt>http://buildbot.example.org:8080?branch=beta4&amp;builder=unix&amp;builder=macos</tt>.</p>
246 <h2>Limiting the Displayed Interval</h2>
248 <p>The <tt>last_time=</tt> argument is a unix timestamp (seconds since the
249 start of 1970) that will be used as an upper bound on the interval of events
250 displayed: nothing will be shown that is more recent than the given time.
251 When no argument is provided, all events up to and including the most recent
252 steps are included.</p>
254 <p>The <tt>first_time=</tt> argument provides the lower bound. No events will
255 be displayed that occurred <b>before</b> this timestamp. Instead of providing
256 <tt>first_time=</tt>, you can provide <tt>show_time=</tt>: in this case,
257 <tt>first_time</tt> will be set equal to <tt>last_time</tt> minus
258 <tt>show_time</tt>. <tt>show_time</tt> overrides <tt>first_time</tt>.</p>
260 <p>The display normally shows the latest 200 events that occurred in the
261 given interval, where each timestamp on the left hand edge counts as a single
262 event. You can add a <tt>num_events=</tt> argument to override this this.</p>
264 <h2>Hiding non-Build events</h2>
266 <p>By passing <tt>show_events=false</tt>, you can remove the "buildslave
267 attached", "buildslave detached", and "builder reconfigured" events that
268 appear in-between the actual builds.</p>
270 %(show_events_input)s
272 <h2>Showing only the Builders with failures</h2>
274 <p>By adding the <tt>failures_only=true</tt> argument, the display will be limited
275 to showing builders that are currently failing. A builder is considered
276 failing if the last finished build was not successful, a step in the current
277 build(s) failed, or if the builder is offline.
279 %(failures_only_input)s
281 <h2>Showing only Certain Branches</h2>
283 <p>If you provide one or more <tt>branch=</tt> arguments, the display will be
284 limited to builds that used one of the given branches. If no <tt>branch=</tt>
285 arguments are given, builds from all branches will be displayed.</p>
287 Erase the text from these "Show Branch:" boxes to remove that branch filter.
289 %(show_branches_input)s
291 <h2>Limiting the Builders that are Displayed</h2>
293 <p>By adding one or more <tt>builder=</tt> arguments, the display will be
294 limited to showing builds that ran on the given builders. This serves to
295 limit the display to the specific named columns. If no <tt>builder=</tt>
296 arguments are provided, all Builders will be displayed.</p>
298 <p>To view a Waterfall page with only a subset of Builders displayed, select
299 the Builders you are interested in here.</p>
301 %(show_builders_input)s
304 <h2>Auto-reloading the Page</h2>
306 <p>Adding a <tt>reload=</tt> argument will cause the page to automatically
307 reload itself after that many seconds.</p>
309 %(show_reload_input)s
311 <h2>Reload Waterfall Page</h2>
313 <input type="submit" value="View Waterfall" />
314 </form>
317 class WaterfallHelp(HtmlResource):
318 title = "Waterfall Help"
320 def __init__(self, categories=None):
321 HtmlResource.__init__(self)
322 self.categories = categories
324 def body(self, request):
325 data = ''
326 status = self.getStatus(request)
328 showEvents_checked = 'checked="checked"'
329 if request.args.get("show_events", ["true"])[0].lower() == "true":
330 showEvents_checked = ''
331 show_events_input = ('<p>'
332 '<input type="checkbox" name="show_events" '
333 'value="false" %s>'
334 'Hide non-Build events'
335 '</p>\n'
336 ) % showEvents_checked
338 failuresOnly_checked = ''
339 if request.args.get("failures_only", ["false"])[0].lower() == "true":
340 failuresOnly_checked = 'checked="checked"'
341 failures_only_input = ('<p>'
342 '<input type="checkbox" name="failures_only" '
343 'value="true" %s>'
344 'Show failures only'
345 '</p>\n'
346 ) % failuresOnly_checked
348 branches = [b
349 for b in request.args.get("branch", [])
350 if b]
351 branches.append('')
352 show_branches_input = '<table>\n'
353 for b in branches:
354 show_branches_input += ('<tr>'
355 '<td>Show Branch: '
356 '<input type="text" name="branch" '
357 'value="%s">'
358 '</td></tr>\n'
359 ) % (b,)
360 show_branches_input += '</table>\n'
362 # this has a set of toggle-buttons to let the user choose the
363 # builders
364 showBuilders = request.args.get("show", [])
365 showBuilders.extend(request.args.get("builder", []))
366 allBuilders = status.getBuilderNames(categories=self.categories)
368 show_builders_input = '<table>\n'
369 for bn in allBuilders:
370 checked = ""
371 if bn in showBuilders:
372 checked = 'checked="checked"'
373 show_builders_input += ('<tr>'
374 '<td><input type="checkbox"'
375 ' name="builder" '
376 'value="%s" %s></td> '
377 '<td>%s</td></tr>\n'
378 ) % (bn, checked, bn)
379 show_builders_input += '</table>\n'
381 # a couple of radio-button selectors for refresh time will appear
382 # just after that text
383 show_reload_input = '<table>\n'
384 times = [("none", "None"),
385 ("60", "60 seconds"),
386 ("300", "5 minutes"),
387 ("600", "10 minutes"),
389 current_reload_time = request.args.get("reload", ["none"])
390 if current_reload_time:
391 current_reload_time = current_reload_time[0]
392 if current_reload_time not in [t[0] for t in times]:
393 times.insert(0, (current_reload_time, current_reload_time) )
394 for value, name in times:
395 checked = ""
396 if value == current_reload_time:
397 checked = 'checked="checked"'
398 show_reload_input += ('<tr>'
399 '<td><input type="radio" name="reload" '
400 'value="%s" %s></td> '
401 '<td>%s</td></tr>\n'
402 ) % (value, checked, name)
403 show_reload_input += '</table>\n'
405 fields = {"show_events_input": show_events_input,
406 "show_branches_input": show_branches_input,
407 "show_builders_input": show_builders_input,
408 "show_reload_input": show_reload_input,
409 "failures_only_input": failures_only_input,
411 data += HELP % fields
412 return data
414 class WaterfallStatusResource(HtmlResource):
415 """This builds the main status page, with the waterfall display, and
416 all child pages."""
418 def __init__(self, categories=None):
419 HtmlResource.__init__(self)
420 self.categories = categories
421 self.putChild("help", WaterfallHelp(categories))
423 def getTitle(self, request):
424 status = self.getStatus(request)
425 p = status.getProjectName()
426 if p:
427 return "BuildBot: %s" % p
428 else:
429 return "BuildBot"
431 def getChangemaster(self, request):
432 # TODO: this wants to go away, access it through IStatus
433 return request.site.buildbot_service.getChangeSvc()
435 def get_reload_time(self, request):
436 if "reload" in request.args:
437 try:
438 reload_time = int(request.args["reload"][0])
439 return max(reload_time, 15)
440 except ValueError:
441 pass
442 return None
444 def head(self, request):
445 head = ''
446 reload_time = self.get_reload_time(request)
447 if reload_time is not None:
448 head += '<meta http-equiv="refresh" content="%d">\n' % reload_time
449 return head
451 def isSuccess(self, builderStatus):
452 # Helper function to return True if the builder is not failing.
453 # The function will return false if the current state is "offline",
454 # the last build was not successful, or if a step from the current
455 # build(s) failed.
457 # Make sure the builder is online.
458 if builderStatus.getState()[0] == 'offline':
459 return False
461 # Look at the last finished build to see if it was success or not.
462 lastBuild = builderStatus.getLastFinishedBuild()
463 if lastBuild and lastBuild.getResults() != builder.SUCCESS:
464 return False
466 # Check all the current builds to see if one step is already
467 # failing.
468 currentBuilds = builderStatus.getCurrentBuilds()
469 if currentBuilds:
470 for build in currentBuilds:
471 for step in build.getSteps():
472 if step.getResults()[0] == builder.FAILURE:
473 return False
475 # The last finished build was successful, and all the current builds
476 # don't have any failed steps.
477 return True
479 def body(self, request):
480 "This method builds the main waterfall display."
482 status = self.getStatus(request)
483 data = ''
485 projectName = status.getProjectName()
486 projectURL = status.getProjectURL()
488 phase = request.args.get("phase",["2"])
489 phase = int(phase[0])
491 # we start with all Builders available to this Waterfall: this is
492 # limited by the config-file -time categories= argument, and defaults
493 # to all defined Builders.
494 allBuilderNames = status.getBuilderNames(categories=self.categories)
495 builders = [status.getBuilder(name) for name in allBuilderNames]
497 # but if the URL has one or more builder= arguments (or the old show=
498 # argument, which is still accepted for backwards compatibility), we
499 # use that set of builders instead. We still don't show anything
500 # outside the config-file time set limited by categories=.
501 showBuilders = request.args.get("show", [])
502 showBuilders.extend(request.args.get("builder", []))
503 if showBuilders:
504 builders = [b for b in builders if b.name in showBuilders]
506 # now, if the URL has one or category= arguments, use them as a
507 # filter: only show those builders which belong to one of the given
508 # categories.
509 showCategories = request.args.get("category", [])
510 if showCategories:
511 builders = [b for b in builders if b.category in showCategories]
513 # If the URL has the failures_only=true argument, we remove all the
514 # builders that are not currently red or won't be turning red at the end
515 # of their current run.
516 failuresOnly = request.args.get("failures_only", ["false"])[0]
517 if failuresOnly.lower() == "true":
518 builders = [b for b in builders if not self.isSuccess(b)]
520 builderNames = [b.name for b in builders]
522 if phase == -1:
523 return self.body0(request, builders)
524 (changeNames, builderNames, timestamps, eventGrid, sourceEvents) = \
525 self.buildGrid(request, builders)
526 if phase == 0:
527 return self.phase0(request, (changeNames + builderNames),
528 timestamps, eventGrid)
529 # start the table: top-header material
530 data += '<table border="0" cellspacing="0">\n'
532 if projectName and projectURL:
533 # TODO: this is going to look really ugly
534 topleft = '<a href="%s">%s</a><br />last build' % \
535 (projectURL, projectName)
536 else:
537 topleft = "last build"
538 data += ' <tr class="LastBuild">\n'
539 data += td(topleft, align="right", colspan=2, class_="Project")
540 for b in builders:
541 box = ITopBox(b).getBox(request)
542 data += box.td(align="center")
543 data += " </tr>\n"
545 data += ' <tr class="Activity">\n'
546 data += td('current activity', align='right', colspan=2)
547 for b in builders:
548 box = ICurrentBox(b).getBox(status)
549 data += box.td(align="center")
550 data += " </tr>\n"
552 data += " <tr>\n"
553 TZ = time.tzname[time.localtime()[-1]]
554 data += td("time (%s)" % TZ, align="center", class_="Time")
555 data += td('<a href="%s">changes</a>' % request.childLink("../changes"),
556 align="center", class_="Change")
557 for name in builderNames:
558 safename = urllib.quote(name, safe='')
559 data += td('<a href="%s">%s</a>' %
560 (request.childLink("../builders/%s" % safename), name),
561 align="center", class_="Builder")
562 data += " </tr>\n"
564 if phase == 1:
565 f = self.phase1
566 else:
567 f = self.phase2
568 data += f(request, changeNames + builderNames, timestamps, eventGrid,
569 sourceEvents)
571 data += "</table>\n"
573 data += '<hr /><div class="footer">\n'
575 def with_args(req, remove_args=[], new_args=[], new_path=None):
576 # sigh, nevow makes this sort of manipulation easier
577 newargs = req.args.copy()
578 for argname in remove_args:
579 newargs[argname] = []
580 if "branch" in newargs:
581 newargs["branch"] = [b for b in newargs["branch"] if b]
582 for k,v in new_args:
583 if k in newargs:
584 newargs[k].append(v)
585 else:
586 newargs[k] = [v]
587 newquery = "&".join(["%s=%s" % (k, v)
588 for k in newargs
589 for v in newargs[k]
591 if new_path:
592 new_url = new_path
593 elif req.prepath:
594 new_url = req.prepath[-1]
595 else:
596 new_url = ''
597 if newquery:
598 new_url += "?" + newquery
599 return new_url
601 if timestamps:
602 bottom = timestamps[-1]
603 nextpage = with_args(request, ["last_time"],
604 [("last_time", str(int(bottom)))])
605 data += '[<a href="%s">next page</a>]\n' % nextpage
607 helpurl = self.path_to_root(request) + "waterfall/help"
608 helppage = with_args(request, new_path=helpurl)
609 data += '[<a href="%s">help</a>]\n' % helppage
611 welcomeurl = self.path_to_root(request) + "index.html"
612 data += '[<a href="%s">welcome</a>]\n' % welcomeurl
614 if self.get_reload_time(request) is not None:
615 no_reload_page = with_args(request, remove_args=["reload"])
616 data += '[<a href="%s">Stop Reloading</a>]\n' % no_reload_page
618 data += "<br />\n"
621 bburl = "http://buildbot.net/?bb-ver=%s" % urllib.quote(version)
622 data += '<a href="%s">Buildbot-%s</a> ' % (bburl, version)
623 if projectName:
624 data += "working for the "
625 if projectURL:
626 data += '<a href="%s">%s</a> project.' % (projectURL,
627 projectName)
628 else:
629 data += "%s project." % projectName
630 data += "<br />\n"
631 # TODO: push this to the right edge, if possible
632 data += ("Page built: " +
633 time.strftime("%a %d %b %Y %H:%M:%S",
634 time.localtime(util.now()))
635 + "\n")
636 data += '</div>\n'
637 return data
639 def body0(self, request, builders):
640 # build the waterfall display
641 data = ""
642 data += "<h2>Basic display</h2>\n"
643 data += '<p>See <a href="%s">here</a>' % request.childLink("../waterfall")
644 data += " for the waterfall display</p>\n"
646 data += '<table border="0" cellspacing="0">\n'
647 names = map(lambda builder: builder.name, builders)
649 # the top row is two blank spaces, then the top-level status boxes
650 data += " <tr>\n"
651 data += td("", colspan=2)
652 for b in builders:
653 text = ""
654 state, builds = b.getState()
655 if state != "offline":
656 text += "%s<br />\n" % state #b.getCurrentBig().text[0]
657 else:
658 text += "OFFLINE<br />\n"
659 data += td(text, align="center")
661 # the next row has the column headers: time, changes, builder names
662 data += " <tr>\n"
663 data += td("Time", align="center")
664 data += td("Changes", align="center")
665 for name in names:
666 data += td('<a href="%s">%s</a>' %
667 (request.childLink("../" + urllib.quote(name)), name),
668 align="center")
669 data += " </tr>\n"
671 # all further rows involve timestamps, commit events, and build events
672 data += " <tr>\n"
673 data += td("04:00", align="bottom")
674 data += td("fred", align="center")
675 for name in names:
676 data += td("stuff", align="center")
677 data += " </tr>\n"
679 data += "</table>\n"
680 return data
682 def buildGrid(self, request, builders):
683 debug = False
684 # TODO: see if we can use a cached copy
686 showEvents = False
687 if request.args.get("show_events", ["true"])[0].lower() == "true":
688 showEvents = True
689 filterBranches = [b for b in request.args.get("branch", []) if b]
690 filterBranches = map_branches(filterBranches)
691 maxTime = int(request.args.get("last_time", [util.now()])[0])
692 if "show_time" in request.args:
693 minTime = maxTime - int(request.args["show_time"][0])
694 elif "first_time" in request.args:
695 minTime = int(request.args["first_time"][0])
696 else:
697 minTime = None
698 spanLength = 10 # ten-second chunks
699 maxPageLen = int(request.args.get("num_events", [200])[0])
701 # first step is to walk backwards in time, asking each column
702 # (commit, all builders) if they have any events there. Build up the
703 # array of events, and stop when we have a reasonable number.
705 commit_source = self.getChangemaster(request)
707 lastEventTime = util.now()
708 sources = [commit_source] + builders
709 changeNames = ["changes"]
710 builderNames = map(lambda builder: builder.getName(), builders)
711 sourceNames = changeNames + builderNames
712 sourceEvents = []
713 sourceGenerators = []
715 def get_event_from(g):
716 try:
717 while True:
718 e = g.next()
719 # e might be builder.BuildStepStatus,
720 # builder.BuildStatus, builder.Event,
721 # waterfall.Spacer(builder.Event), or changes.Change .
722 # The showEvents=False flag means we should hide
723 # builder.Event .
724 if not showEvents and isinstance(e, builder.Event):
725 continue
726 break
727 event = interfaces.IStatusEvent(e)
728 if debug:
729 log.msg("gen %s gave1 %s" % (g, event.getText()))
730 except StopIteration:
731 event = None
732 return event
734 for s in sources:
735 gen = insertGaps(s.eventGenerator(filterBranches), lastEventTime)
736 sourceGenerators.append(gen)
737 # get the first event
738 sourceEvents.append(get_event_from(gen))
739 eventGrid = []
740 timestamps = []
742 lastEventTime = 0
743 for e in sourceEvents:
744 if e and e.getTimes()[0] > lastEventTime:
745 lastEventTime = e.getTimes()[0]
746 if lastEventTime == 0:
747 lastEventTime = util.now()
749 spanStart = lastEventTime - spanLength
750 debugGather = 0
752 while 1:
753 if debugGather: log.msg("checking (%s,]" % spanStart)
754 # the tableau of potential events is in sourceEvents[]. The
755 # window crawls backwards, and we examine one source at a time.
756 # If the source's top-most event is in the window, is it pushed
757 # onto the events[] array and the tableau is refilled. This
758 # continues until the tableau event is not in the window (or is
759 # missing).
761 spanEvents = [] # for all sources, in this span. row of eventGrid
762 firstTimestamp = None # timestamp of first event in the span
763 lastTimestamp = None # last pre-span event, for next span
765 for c in range(len(sourceGenerators)):
766 events = [] # for this source, in this span. cell of eventGrid
767 event = sourceEvents[c]
768 while event and spanStart < event.getTimes()[0]:
769 # to look at windows that don't end with the present,
770 # condition the .append on event.time <= spanFinish
771 if not IBox(event, None):
772 log.msg("BAD EVENT", event, event.getText())
773 assert 0
774 if debug:
775 log.msg("pushing", event.getText(), event)
776 events.append(event)
777 starts, finishes = event.getTimes()
778 firstTimestamp = util.earlier(firstTimestamp, starts)
779 event = get_event_from(sourceGenerators[c])
780 if debug:
781 log.msg("finished span")
783 if event:
784 # this is the last pre-span event for this source
785 lastTimestamp = util.later(lastTimestamp,
786 event.getTimes()[0])
787 if debugGather:
788 log.msg(" got %s from %s" % (events, sourceNames[c]))
789 sourceEvents[c] = event # refill the tableau
790 spanEvents.append(events)
792 # only show events older than maxTime. This makes it possible to
793 # visit a page that shows what it would be like to scroll off the
794 # bottom of this one.
795 if firstTimestamp is not None and firstTimestamp <= maxTime:
796 eventGrid.append(spanEvents)
797 timestamps.append(firstTimestamp)
799 if lastTimestamp:
800 spanStart = lastTimestamp - spanLength
801 else:
802 # no more events
803 break
804 if minTime is not None and lastTimestamp < minTime:
805 break
807 if len(timestamps) > maxPageLen:
808 break
811 # now loop
813 # loop is finished. now we have eventGrid[] and timestamps[]
814 if debugGather: log.msg("finished loop")
815 assert(len(timestamps) == len(eventGrid))
816 return (changeNames, builderNames, timestamps, eventGrid, sourceEvents)
818 def phase0(self, request, sourceNames, timestamps, eventGrid):
819 # phase0 rendering
820 if not timestamps:
821 return "no events"
822 data = ""
823 for r in range(0, len(timestamps)):
824 data += "<p>\n"
825 data += "[%s]<br />" % timestamps[r]
826 row = eventGrid[r]
827 assert(len(row) == len(sourceNames))
828 for c in range(0, len(row)):
829 if row[c]:
830 data += "<b>%s</b><br />\n" % sourceNames[c]
831 for e in row[c]:
832 log.msg("Event", r, c, sourceNames[c], e.getText())
833 lognames = [loog.getName() for loog in e.getLogs()]
834 data += "%s: %s: %s<br />" % (e.getText(),
835 e.getTimes()[0],
836 lognames)
837 else:
838 data += "<b>%s</b> [none]<br />\n" % sourceNames[c]
839 return data
841 def phase1(self, request, sourceNames, timestamps, eventGrid,
842 sourceEvents):
843 # phase1 rendering: table, but boxes do not overlap
844 data = ""
845 if not timestamps:
846 return data
847 lastDate = None
848 for r in range(0, len(timestamps)):
849 chunkstrip = eventGrid[r]
850 # chunkstrip is a horizontal strip of event blocks. Each block
851 # is a vertical list of events, all for the same source.
852 assert(len(chunkstrip) == len(sourceNames))
853 maxRows = reduce(lambda x,y: max(x,y),
854 map(lambda x: len(x), chunkstrip))
855 for i in range(maxRows):
856 data += " <tr>\n";
857 if i == 0:
858 stuff = []
859 # add the date at the beginning, and each time it changes
860 today = time.strftime("<b>%d %b %Y</b>",
861 time.localtime(timestamps[r]))
862 todayday = time.strftime("<b>%a</b>",
863 time.localtime(timestamps[r]))
864 if today != lastDate:
865 stuff.append(todayday)
866 stuff.append(today)
867 lastDate = today
868 stuff.append(
869 time.strftime("%H:%M:%S",
870 time.localtime(timestamps[r])))
871 data += td(stuff, valign="bottom", align="center",
872 rowspan=maxRows, class_="Time")
873 for c in range(0, len(chunkstrip)):
874 block = chunkstrip[c]
875 assert(block != None) # should be [] instead
876 # bottom-justify
877 offset = maxRows - len(block)
878 if i < offset:
879 data += td("")
880 else:
881 e = block[i-offset]
882 box = IBox(e).getBox(request)
883 box.parms["show_idle"] = 1
884 data += box.td(valign="top", align="center")
885 data += " </tr>\n"
887 return data
889 def phase2(self, request, sourceNames, timestamps, eventGrid,
890 sourceEvents):
891 data = ""
892 if not timestamps:
893 return data
894 # first pass: figure out the height of the chunks, populate grid
895 grid = []
896 for i in range(1+len(sourceNames)):
897 grid.append([])
898 # grid is a list of columns, one for the timestamps, and one per
899 # event source. Each column is exactly the same height. Each element
900 # of the list is a single <td> box.
901 lastDate = time.strftime("<b>%d %b %Y</b>",
902 time.localtime(util.now()))
903 for r in range(0, len(timestamps)):
904 chunkstrip = eventGrid[r]
905 # chunkstrip is a horizontal strip of event blocks. Each block
906 # is a vertical list of events, all for the same source.
907 assert(len(chunkstrip) == len(sourceNames))
908 maxRows = reduce(lambda x,y: max(x,y),
909 map(lambda x: len(x), chunkstrip))
910 for i in range(maxRows):
911 if i != maxRows-1:
912 grid[0].append(None)
913 else:
914 # timestamp goes at the bottom of the chunk
915 stuff = []
916 # add the date at the beginning (if it is not the same as
917 # today's date), and each time it changes
918 todayday = time.strftime("<b>%a</b>",
919 time.localtime(timestamps[r]))
920 today = time.strftime("<b>%d %b %Y</b>",
921 time.localtime(timestamps[r]))
922 if today != lastDate:
923 stuff.append(todayday)
924 stuff.append(today)
925 lastDate = today
926 stuff.append(
927 time.strftime("%H:%M:%S",
928 time.localtime(timestamps[r])))
929 grid[0].append(Box(text=stuff, class_="Time",
930 valign="bottom", align="center"))
932 # at this point the timestamp column has been populated with
933 # maxRows boxes, most None but the last one has the time string
934 for c in range(0, len(chunkstrip)):
935 block = chunkstrip[c]
936 assert(block != None) # should be [] instead
937 for i in range(maxRows - len(block)):
938 # fill top of chunk with blank space
939 grid[c+1].append(None)
940 for i in range(len(block)):
941 # so the events are bottom-justified
942 b = IBox(block[i]).getBox(request)
943 b.parms['valign'] = "top"
944 b.parms['align'] = "center"
945 grid[c+1].append(b)
946 # now all the other columns have maxRows new boxes too
947 # populate the last row, if empty
948 gridlen = len(grid[0])
949 for i in range(len(grid)):
950 strip = grid[i]
951 assert(len(strip) == gridlen)
952 if strip[-1] == None:
953 if sourceEvents[i-1]:
954 filler = IBox(sourceEvents[i-1]).getBox(request)
955 else:
956 # this can happen if you delete part of the build history
957 filler = Box(text=["?"], align="center")
958 strip[-1] = filler
959 strip[-1].parms['rowspan'] = 1
960 # second pass: bubble the events upwards to un-occupied locations
961 # Every square of the grid that has a None in it needs to have
962 # something else take its place.
963 noBubble = request.args.get("nobubble",['0'])
964 noBubble = int(noBubble[0])
965 if not noBubble:
966 for col in range(len(grid)):
967 strip = grid[col]
968 if col == 1: # changes are handled differently
969 for i in range(2, len(strip)+1):
970 # only merge empty boxes. Don't bubble commit boxes.
971 if strip[-i] == None:
972 next = strip[-i+1]
973 assert(next)
974 if next:
975 #if not next.event:
976 if next.spacer:
977 # bubble the empty box up
978 strip[-i] = next
979 strip[-i].parms['rowspan'] += 1
980 strip[-i+1] = None
981 else:
982 # we are above a commit box. Leave it
983 # be, and turn the current box into an
984 # empty one
985 strip[-i] = Box([], rowspan=1,
986 comment="commit bubble")
987 strip[-i].spacer = True
988 else:
989 # we are above another empty box, which
990 # somehow wasn't already converted.
991 # Shouldn't happen
992 pass
993 else:
994 for i in range(2, len(strip)+1):
995 # strip[-i] will go from next-to-last back to first
996 if strip[-i] == None:
997 # bubble previous item up
998 assert(strip[-i+1] != None)
999 strip[-i] = strip[-i+1]
1000 strip[-i].parms['rowspan'] += 1
1001 strip[-i+1] = None
1002 else:
1003 strip[-i].parms['rowspan'] = 1
1004 # third pass: render the HTML table
1005 for i in range(gridlen):
1006 data += " <tr>\n";
1007 for strip in grid:
1008 b = strip[i]
1009 if b:
1010 # convert data to a unicode string, whacking any non-ASCII characters it might contain
1011 s = b.td()
1012 if isinstance(s, unicode):
1013 s = s.encode("utf-8", "replace")
1014 data += s
1015 else:
1016 if noBubble:
1017 data += td([])
1018 # Nones are left empty, rowspan should make it all fit
1019 data += " </tr>\n"
1020 return data