Package buildbot :: Package status :: Package web :: Module waterfall
[hide private]
[frames] | no frames]

Source Code for Module buildbot.status.web.waterfall

   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, 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 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)
230 231 HELP = ''' 232 <form action="../waterfall" method="GET"> 233 234 <h1>The Waterfall Display</h1> 235 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> 245 246 <h2>Limiting the Displayed Interval</h2> 247 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> 253 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> 259 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> 263 264 <h2>Hiding non-Build events</h2> 265 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> 269 270 %(show_events_input)s 271 272 <h2>Showing only the Builders with failures</h2> 273 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. 278 279 %(failures_only_input)s 280 281 <h2>Showing only Certain Branches</h2> 282 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> 286 287 Erase the text from these "Show Branch:" boxes to remove that branch filter. 288 289 %(show_branches_input)s 290 291 <h2>Limiting the Builders that are Displayed</h2> 292 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> 297 298 <p>To view a Waterfall page with only a subset of Builders displayed, select 299 the Builders you are interested in here.</p> 300 301 %(show_builders_input)s 302 303 304 <h2>Auto-reloading the Page</h2> 305 306 <p>Adding a <tt>reload=</tt> argument will cause the page to automatically 307 reload itself after that many seconds.</p> 308 309 %(show_reload_input)s 310 311 <h2>Reload Waterfall Page</h2> 312 313 <input type="submit" value="View Waterfall" /> 314 </form> 315 ''' 316
317 -class WaterfallHelp(HtmlResource):
318 title = "Waterfall Help" 319
320 - def __init__(self, categories=None):
321 HtmlResource.__init__(self) 322 self.categories = categories
323
324 - def body(self, request):
325 data = '' 326 status = self.getStatus(request) 327 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 337 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 347 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' 361 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) 367 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' 380 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"), 388 ] 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' 404 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, 410 } 411 data += HELP % fields 412 return data
413
414 -class WaterfallStatusResource(HtmlResource):
415 """This builds the main status page, with the waterfall display, and 416 all child pages.""" 417
418 - def __init__(self, categories=None):
419 HtmlResource.__init__(self) 420 self.categories = categories 421 self.putChild("help", WaterfallHelp(categories))
422
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"
430
431 - def getChangemaster(self, request):
432 # TODO: this wants to go away, access it through IStatus 433 return request.site.buildbot_service.getChangeSvc()
434
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
443
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
450
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. 456 457 # Make sure the builder is online. 458 if builderStatus.getState()[0] == 'offline': 459 return False 460 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 465 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 474 475 # The last finished build was successful, and all the current builds 476 # don't have any failed steps. 477 return True
478
479 - def body(self, request):
480 "This method builds the main waterfall display." 481 482 status = self.getStatus(request) 483 data = '' 484 485 projectName = status.getProjectName() 486 projectURL = status.getProjectURL() 487 488 phase = request.args.get("phase",["2"]) 489 phase = int(phase[0]) 490 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] 496 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] 505 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] 512 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)] 519 520 builderNames = [b.name for b in builders] 521 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' 531 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" 544 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" 551 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" 563 564 if phase == 1: 565 f = self.phase1 566 else: 567 f = self.phase2 568 data += f(request, changeNames + builderNames, timestamps, eventGrid, 569 sourceEvents) 570 571 data += "</table>\n" 572 573 data += '<hr /><div class="footer">\n' 574 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] 590 ]) 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
600 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 606 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 610 611 welcomeurl = self.path_to_root(request) + "index.html" 612 data += '[<a href="%s">welcome</a>]\n' % welcomeurl 613 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 617 618 data += "<br />\n" 619 620 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
638
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" 645 646 data += '<table border="0" cellspacing="0">\n' 647 names = map(lambda builder: builder.name, builders) 648 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") 660 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" 670 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" 678 679 data += "</table>\n" 680 return data
681
682 - def buildGrid(self, request, builders):
683 debug = False 684 # TODO: see if we can use a cached copy 685 686 showEvents = False 687 if request.args.get("show_events", ["true"])[0].lower() == "true": 688 showEvents = True 689 filterCategories = request.args.get('category', []) 690 filterBranches = [b for b in request.args.get("branch", []) if b] 691 filterBranches = map_branches(filterBranches) 692 maxTime = int(request.args.get("last_time", [util.now()])[0]) 693 if "show_time" in request.args: 694 minTime = maxTime - int(request.args["show_time"][0]) 695 elif "first_time" in request.args: 696 minTime = int(request.args["first_time"][0]) 697 else: 698 minTime = None 699 spanLength = 10 # ten-second chunks 700 maxPageLen = int(request.args.get("num_events", [200])[0]) 701 702 # first step is to walk backwards in time, asking each column 703 # (commit, all builders) if they have any events there. Build up the 704 # array of events, and stop when we have a reasonable number. 705 706 commit_source = self.getChangemaster(request) 707 708 lastEventTime = util.now() 709 sources = [commit_source] + builders 710 changeNames = ["changes"] 711 builderNames = map(lambda builder: builder.getName(), builders) 712 sourceNames = changeNames + builderNames 713 sourceEvents = [] 714 sourceGenerators = [] 715 716 def get_event_from(g): 717 try: 718 while True: 719 e = g.next() 720 # e might be builder.BuildStepStatus, 721 # builder.BuildStatus, builder.Event, 722 # waterfall.Spacer(builder.Event), or changes.Change . 723 # The showEvents=False flag means we should hide 724 # builder.Event . 725 if not showEvents and isinstance(e, builder.Event): 726 continue 727 break 728 event = interfaces.IStatusEvent(e) 729 if debug: 730 log.msg("gen %s gave1 %s" % (g, event.getText())) 731 except StopIteration: 732 event = None 733 return event
734 735 for s in sources: 736 gen = insertGaps(s.eventGenerator(filterBranches, filterCategories), lastEventTime) 737 sourceGenerators.append(gen) 738 # get the first event 739 sourceEvents.append(get_event_from(gen)) 740 eventGrid = [] 741 timestamps = [] 742 743 lastEventTime = 0 744 for e in sourceEvents: 745 if e and e.getTimes()[0] > lastEventTime: 746 lastEventTime = e.getTimes()[0] 747 if lastEventTime == 0: 748 lastEventTime = util.now() 749 750 spanStart = lastEventTime - spanLength 751 debugGather = 0 752 753 while 1: 754 if debugGather: log.msg("checking (%s,]" % spanStart) 755 # the tableau of potential events is in sourceEvents[]. The 756 # window crawls backwards, and we examine one source at a time. 757 # If the source's top-most event is in the window, is it pushed 758 # onto the events[] array and the tableau is refilled. This 759 # continues until the tableau event is not in the window (or is 760 # missing). 761 762 spanEvents = [] # for all sources, in this span. row of eventGrid 763 firstTimestamp = None # timestamp of first event in the span 764 lastTimestamp = None # last pre-span event, for next span 765 766 for c in range(len(sourceGenerators)): 767 events = [] # for this source, in this span. cell of eventGrid 768 event = sourceEvents[c] 769 while event and spanStart < event.getTimes()[0]: 770 # to look at windows that don't end with the present, 771 # condition the .append on event.time <= spanFinish 772 if not IBox(event, None): 773 log.msg("BAD EVENT", event, event.getText()) 774 assert 0 775 if debug: 776 log.msg("pushing", event.getText(), event) 777 events.append(event) 778 starts, finishes = event.getTimes() 779 firstTimestamp = util.earlier(firstTimestamp, starts) 780 event = get_event_from(sourceGenerators[c]) 781 if debug: 782 log.msg("finished span") 783 784 if event: 785 # this is the last pre-span event for this source 786 lastTimestamp = util.later(lastTimestamp, 787 event.getTimes()[0]) 788 if debugGather: 789 log.msg(" got %s from %s" % (events, sourceNames[c])) 790 sourceEvents[c] = event # refill the tableau 791 spanEvents.append(events) 792 793 # only show events older than maxTime. This makes it possible to 794 # visit a page that shows what it would be like to scroll off the 795 # bottom of this one. 796 if firstTimestamp is not None and firstTimestamp <= maxTime: 797 eventGrid.append(spanEvents) 798 timestamps.append(firstTimestamp) 799 800 if lastTimestamp: 801 spanStart = lastTimestamp - spanLength 802 else: 803 # no more events 804 break 805 if minTime is not None and lastTimestamp < minTime: 806 break 807 808 if len(timestamps) > maxPageLen: 809 break 810 811 812 # now loop 813 814 # loop is finished. now we have eventGrid[] and timestamps[] 815 if debugGather: log.msg("finished loop") 816 assert(len(timestamps) == len(eventGrid)) 817 return (changeNames, builderNames, timestamps, eventGrid, sourceEvents) 818
819 - def phase0(self, request, sourceNames, timestamps, eventGrid):
820 # phase0 rendering 821 if not timestamps: 822 return "no events" 823 data = "" 824 for r in range(0, len(timestamps)): 825 data += "<p>\n" 826 data += "[%s]<br />" % timestamps[r] 827 row = eventGrid[r] 828 assert(len(row) == len(sourceNames)) 829 for c in range(0, len(row)): 830 if row[c]: 831 data += "<b>%s</b><br />\n" % sourceNames[c] 832 for e in row[c]: 833 log.msg("Event", r, c, sourceNames[c], e.getText()) 834 lognames = [loog.getName() for loog in e.getLogs()] 835 data += "%s: %s: %s<br />" % (e.getText(), 836 e.getTimes()[0], 837 lognames) 838 else: 839 data += "<b>%s</b> [none]<br />\n" % sourceNames[c] 840 return data
841
842 - def phase1(self, request, sourceNames, timestamps, eventGrid, 843 sourceEvents):
844 # phase1 rendering: table, but boxes do not overlap 845 data = "" 846 if not timestamps: 847 return data 848 lastDate = None 849 for r in range(0, len(timestamps)): 850 chunkstrip = eventGrid[r] 851 # chunkstrip is a horizontal strip of event blocks. Each block 852 # is a vertical list of events, all for the same source. 853 assert(len(chunkstrip) == len(sourceNames)) 854 maxRows = reduce(lambda x,y: max(x,y), 855 map(lambda x: len(x), chunkstrip)) 856 for i in range(maxRows): 857 data += " <tr>\n"; 858 if i == 0: 859 stuff = [] 860 # add the date at the beginning, and each time it changes 861 today = time.strftime("<b>%d %b %Y</b>", 862 time.localtime(timestamps[r])) 863 todayday = time.strftime("<b>%a</b>", 864 time.localtime(timestamps[r])) 865 if today != lastDate: 866 stuff.append(todayday) 867 stuff.append(today) 868 lastDate = today 869 stuff.append( 870 time.strftime("%H:%M:%S", 871 time.localtime(timestamps[r]))) 872 data += td(stuff, valign="bottom", align="center", 873 rowspan=maxRows, class_="Time") 874 for c in range(0, len(chunkstrip)): 875 block = chunkstrip[c] 876 assert(block != None) # should be [] instead 877 # bottom-justify 878 offset = maxRows - len(block) 879 if i < offset: 880 data += td("") 881 else: 882 e = block[i-offset] 883 box = IBox(e).getBox(request) 884 box.parms["show_idle"] = 1 885 data += box.td(valign="top", align="center") 886 data += " </tr>\n" 887 888 return data
889
890 - def phase2(self, request, sourceNames, timestamps, eventGrid, 891 sourceEvents):
892 data = "" 893 if not timestamps: 894 return data 895 # first pass: figure out the height of the chunks, populate grid 896 grid = [] 897 for i in range(1+len(sourceNames)): 898 grid.append([]) 899 # grid is a list of columns, one for the timestamps, and one per 900 # event source. Each column is exactly the same height. Each element 901 # of the list is a single <td> box. 902 lastDate = time.strftime("<b>%d %b %Y</b>", 903 time.localtime(util.now())) 904 for r in range(0, len(timestamps)): 905 chunkstrip = eventGrid[r] 906 # chunkstrip is a horizontal strip of event blocks. Each block 907 # is a vertical list of events, all for the same source. 908 assert(len(chunkstrip) == len(sourceNames)) 909 maxRows = reduce(lambda x,y: max(x,y), 910 map(lambda x: len(x), chunkstrip)) 911 for i in range(maxRows): 912 if i != maxRows-1: 913 grid[0].append(None) 914 else: 915 # timestamp goes at the bottom of the chunk 916 stuff = [] 917 # add the date at the beginning (if it is not the same as 918 # today's date), and each time it changes 919 todayday = time.strftime("<b>%a</b>", 920 time.localtime(timestamps[r])) 921 today = time.strftime("<b>%d %b %Y</b>", 922 time.localtime(timestamps[r])) 923 if today != lastDate: 924 stuff.append(todayday) 925 stuff.append(today) 926 lastDate = today 927 stuff.append( 928 time.strftime("%H:%M:%S", 929 time.localtime(timestamps[r]))) 930 grid[0].append(Box(text=stuff, class_="Time", 931 valign="bottom", align="center")) 932 933 # at this point the timestamp column has been populated with 934 # maxRows boxes, most None but the last one has the time string 935 for c in range(0, len(chunkstrip)): 936 block = chunkstrip[c] 937 assert(block != None) # should be [] instead 938 for i in range(maxRows - len(block)): 939 # fill top of chunk with blank space 940 grid[c+1].append(None) 941 for i in range(len(block)): 942 # so the events are bottom-justified 943 b = IBox(block[i]).getBox(request) 944 b.parms['valign'] = "top" 945 b.parms['align'] = "center" 946 grid[c+1].append(b) 947 # now all the other columns have maxRows new boxes too 948 # populate the last row, if empty 949 gridlen = len(grid[0]) 950 for i in range(len(grid)): 951 strip = grid[i] 952 assert(len(strip) == gridlen) 953 if strip[-1] == None: 954 if sourceEvents[i-1]: 955 filler = IBox(sourceEvents[i-1]).getBox(request) 956 else: 957 # this can happen if you delete part of the build history 958 filler = Box(text=["?"], align="center") 959 strip[-1] = filler 960 strip[-1].parms['rowspan'] = 1 961 # second pass: bubble the events upwards to un-occupied locations 962 # Every square of the grid that has a None in it needs to have 963 # something else take its place. 964 noBubble = request.args.get("nobubble",['0']) 965 noBubble = int(noBubble[0]) 966 if not noBubble: 967 for col in range(len(grid)): 968 strip = grid[col] 969 if col == 1: # changes are handled differently 970 for i in range(2, len(strip)+1): 971 # only merge empty boxes. Don't bubble commit boxes. 972 if strip[-i] == None: 973 next = strip[-i+1] 974 assert(next) 975 if next: 976 #if not next.event: 977 if next.spacer: 978 # bubble the empty box up 979 strip[-i] = next 980 strip[-i].parms['rowspan'] += 1 981 strip[-i+1] = None 982 else: 983 # we are above a commit box. Leave it 984 # be, and turn the current box into an 985 # empty one 986 strip[-i] = Box([], rowspan=1, 987 comment="commit bubble") 988 strip[-i].spacer = True 989 else: 990 # we are above another empty box, which 991 # somehow wasn't already converted. 992 # Shouldn't happen 993 pass 994 else: 995 for i in range(2, len(strip)+1): 996 # strip[-i] will go from next-to-last back to first 997 if strip[-i] == None: 998 # bubble previous item up 999 assert(strip[-i+1] != None) 1000 strip[-i] = strip[-i+1] 1001 strip[-i].parms['rowspan'] += 1 1002 strip[-i+1] = None 1003 else: 1004 strip[-i].parms['rowspan'] = 1 1005 # third pass: render the HTML table 1006 for i in range(gridlen): 1007 data += " <tr>\n"; 1008 for strip in grid: 1009 b = strip[i] 1010 if b: 1011 # convert data to a unicode string, whacking any non-ASCII characters it might contain 1012 s = b.td() 1013 if isinstance(s, unicode): 1014 s = s.encode("utf-8", "replace") 1015 data += s 1016 else: 1017 if noBubble: 1018 data += td([]) 1019 # Nones are left empty, rowspan should make it all fit 1020 data += " </tr>\n" 1021 return data
1022