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