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
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
):
32 eta_parts
.append("%d hrs" % (eta_secs
/ 3600))
35 eta_parts
.append("%d mins" % (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.
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
:
57 if state
== "building":
62 text
.extend(self
.formatETA("ETA in", eta
))
63 elif state
== "offline":
67 elif state
== "waiting":
70 # just in case I add a state and forget to update this
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()
81 text
.append("%d pending" % len(pbs
))
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
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
),
102 return Box(["none"], class_
="LastBuild")
104 name
= b
.getBuilder().getName()
105 number
= b
.getNumber()
106 url
= path_to_build(req
, b
)
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
120 def getBox(self
, req
):
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
))
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
):
138 def getBox(self
, req
):
139 urlbase
= path_to_step(req
, self
.original
)
140 text
= self
.original
.getText()
142 log
.msg("getText() gave None", urlbase
)
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
)))
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
):
166 def getBox(self
, req
):
167 text
= self
.original
.getText()
169 return Box(text
, class_
=class_
)
170 components
.registerAdapter(EventBox
, builder
.Event
, IBox
)
174 implements(interfaces
.IStatusEvent
)
176 def __init__(self
, start
, finish
):
178 self
.finished
= finish
181 return (self
.started
, self
.finished
)
185 class SpacerBox(components
.Adapter
):
188 def getBox(self
, req
):
189 #b = Box(["spacer"], "white")
193 components
.registerAdapter(SpacerBox
, Spacer
, IBox
)
195 def insertGaps(g
, lastEventTime
, idleGap
=2):
199 starts
, finishes
= e
.getTimes()
200 if debug
: log
.msg("E0", starts
, finishes
)
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
)
215 starts
, finishes
= e
.getTimes()
216 if debug
: log
.msg("E2", starts
, finishes
)
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
224 log
.msg(" finishes=%s, gap=%s, fES=%s" % \
225 (finishes
, idleGap
, followingEventStarts
))
226 yield Spacer(finishes
, followingEventStarts
)
228 followingEventStarts
= starts
229 if debug
: log
.msg(" fES1", starts
)
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&builder=unix&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" />
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
):
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" '
334 'Hide non-Build events'
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" '
346 ) % failuresOnly_checked
349 for b
in request
.args
.get("branch", [])
352 show_branches_input
= '<table>\n'
354 show_branches_input
+= ('<tr>'
356 '<input type="text" name="branch" '
360 show_branches_input
+= '</table>\n'
362 # this has a set of toggle-buttons to let the user choose the
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
:
371 if bn
in showBuilders
:
372 checked
= 'checked="checked"'
373 show_builders_input
+= ('<tr>'
374 '<td><input type="checkbox"'
376 'value="%s" %s></td> '
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
:
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> '
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
414 class WaterfallStatusResource(HtmlResource
):
415 """This builds the main status page, with the waterfall display, and
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()
427 return "BuildBot: %s" % p
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
:
438 reload_time
= int(request
.args
["reload"][0])
439 return max(reload_time
, 15)
444 def head(self
, request
):
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
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
457 # Make sure the builder is online.
458 if builderStatus
.getState()[0] == 'offline':
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
:
466 # Check all the current builds to see if one step is already
468 currentBuilds
= builderStatus
.getCurrentBuilds()
470 for build
in currentBuilds
:
471 for step
in build
.getSteps():
472 if step
.getResults()[0] == builder
.FAILURE
:
475 # The last finished build was successful, and all the current builds
476 # don't have any failed steps.
479 def body(self
, request
):
480 "This method builds the main waterfall display."
482 status
= self
.getStatus(request
)
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", []))
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
509 showCategories
= request
.args
.get("category", [])
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
]
523 return self
.body0(request
, builders
)
524 (changeNames
, builderNames
, timestamps
, eventGrid
, sourceEvents
) = \
525 self
.buildGrid(request
, builders
)
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
)
537 topleft
= "last build"
538 data
+= ' <tr class="LastBuild">\n'
539 data
+= td(topleft
, align
="right", colspan
=2, class_
="Project")
541 box
= ITopBox(b
).getBox(request
)
542 data
+= box
.td(align
="center")
545 data
+= ' <tr class="Activity">\n'
546 data
+= td('current activity', align
='right', colspan
=2)
548 box
= ICurrentBox(b
).getBox(status
)
549 data
+= box
.td(align
="center")
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")
568 data
+= f(request
, changeNames
+ builderNames
, timestamps
, eventGrid
,
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
]
587 newquery
= "&".join(["%s=%s" % (k
, v
)
594 new_url
= req
.prepath
[-1]
598 new_url
+= "?" + newquery
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
621 bburl
= "http://buildbot.net/?bb-ver=%s" % urllib
.quote(version
)
622 data
+= '<a href="%s">Buildbot-%s</a> ' % (bburl
, version
)
624 data
+= "working for the "
626 data
+= '<a href="%s">%s</a> project.' % (projectURL
,
629 data
+= "%s project." % projectName
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()))
639 def body0(self
, request
, builders
):
640 # build the waterfall display
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
651 data
+= td("", colspan
=2)
654 state
, builds
= b
.getState()
655 if state
!= "offline":
656 text
+= "%s<br />\n" % state
#b.getCurrentBig().text[0]
658 text
+= "OFFLINE<br />\n"
659 data
+= td(text
, align
="center")
661 # the next row has the column headers: time, changes, builder names
663 data
+= td("Time", align
="center")
664 data
+= td("Changes", align
="center")
666 data
+= td('<a href="%s">%s</a>' %
667 (request
.childLink("../" + urllib
.quote(name
)), name
),
671 # all further rows involve timestamps, commit events, and build events
673 data
+= td("04:00", align
="bottom")
674 data
+= td("fred", align
="center")
676 data
+= td("stuff", align
="center")
682 def buildGrid(self
, request
, builders
):
684 # TODO: see if we can use a cached copy
687 if request
.args
.get("show_events", ["true"])[0].lower() == "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])
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
713 sourceGenerators
= []
715 def get_event_from(g
):
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
724 if not showEvents
and isinstance(e
, builder
.Event
):
727 event
= interfaces
.IStatusEvent(e
)
729 log
.msg("gen %s gave1 %s" % (g
, event
.getText()))
730 except StopIteration:
735 gen
= insertGaps(s
.eventGenerator(filterBranches
), lastEventTime
)
736 sourceGenerators
.append(gen
)
737 # get the first event
738 sourceEvents
.append(get_event_from(gen
))
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
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
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())
775 log
.msg("pushing", event
.getText(), event
)
777 starts
, finishes
= event
.getTimes()
778 firstTimestamp
= util
.earlier(firstTimestamp
, starts
)
779 event
= get_event_from(sourceGenerators
[c
])
781 log
.msg("finished span")
784 # this is the last pre-span event for this source
785 lastTimestamp
= util
.later(lastTimestamp
,
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
)
800 spanStart
= lastTimestamp
- spanLength
804 if minTime
is not None and lastTimestamp
< minTime
:
807 if len(timestamps
) > maxPageLen
:
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
):
823 for r
in range(0, len(timestamps
)):
825 data
+= "[%s]<br />" % timestamps
[r
]
827 assert(len(row
) == len(sourceNames
))
828 for c
in range(0, len(row
)):
830 data
+= "<b>%s</b><br />\n" % sourceNames
[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(),
838 data
+= "<b>%s</b> [none]<br />\n" % sourceNames
[c
]
841 def phase1(self
, request
, sourceNames
, timestamps
, eventGrid
,
843 # phase1 rendering: table, but boxes do not overlap
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
):
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
)
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
877 offset
= maxRows
- len(block
)
882 box
= IBox(e
).getBox(request
)
883 box
.parms
["show_idle"] = 1
884 data
+= box
.td(valign
="top", align
="center")
889 def phase2(self
, request
, sourceNames
, timestamps
, eventGrid
,
894 # first pass: figure out the height of the chunks, populate grid
896 for i
in range(1+len(sourceNames
)):
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
):
914 # timestamp goes at the bottom of the chunk
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
)
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"
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
)):
951 assert(len(strip
) == gridlen
)
952 if strip
[-1] == None:
953 if sourceEvents
[i
-1]:
954 filler
= IBox(sourceEvents
[i
-1]).getBox(request
)
956 # this can happen if you delete part of the build history
957 filler
= Box(text
=["?"], align
="center")
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])
966 for col
in range(len(grid
)):
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:
977 # bubble the empty box up
979 strip
[-i
].parms
['rowspan'] += 1
982 # we are above a commit box. Leave it
983 # be, and turn the current box into an
985 strip
[-i
] = Box([], rowspan
=1,
986 comment
="commit bubble")
987 strip
[-i
].spacer
= True
989 # we are above another empty box, which
990 # somehow wasn't already converted.
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
1003 strip
[-i
].parms
['rowspan'] = 1
1004 # third pass: render the HTML table
1005 for i
in range(gridlen
):
1010 # convert data to a unicode string, whacking any non-ASCII characters it might contain
1012 if isinstance(s
, unicode):
1013 s
= s
.encode("utf-8", "replace")
1018 # Nones are left empty, rowspan should make it all fit