1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 from zope.interface import implements
18 from twisted.python import log, components
19 from twisted.internet import defer
20 import urllib
21
22 import time, locale
23 import operator
24
25 from buildbot import interfaces, util
26 from buildbot.status import builder, buildstep, build
27 from buildbot.changes import changes
28
29 from buildbot.status.web.base import Box, HtmlResource, IBox, ICurrentBox, \
30 ITopBox, build_get_class, path_to_build, path_to_step, path_to_root, \
31 map_branches
32
33
35
36 if old:
37 if new < old:
38 return new
39 return old
40 return new
41
43
44 if old:
45 if new > old:
46 return new
47 return old
48 return new
49
50
118
119 components.registerAdapter(CurrentBox, builder.BuilderStatus, ICurrentBox)
120
121
123
124
125 implements(IBox)
126
143 components.registerAdapter(BuildTopBox, builder.BuilderStatus, ITopBox)
144
146
147 implements(IBox)
148
162 components.registerAdapter(BuildBox, build.BuildStatus, IBox)
163
194 components.registerAdapter(StepBox, buildstep.BuildStepStatus, IBox)
195
196
198 implements(IBox)
199
201 text = self.original.getText()
202 class_ = "Event"
203 return Box(text, class_=class_)
204 components.registerAdapter(EventBox, builder.Event, IBox)
205
206
218
220 implements(IBox)
221
223
224 b = Box([])
225 b.spacer = True
226 return b
227 components.registerAdapter(SpacerBox, Spacer, IBox)
228
229 -def insertGaps(g, showEvents, lastEventTime, idleGap=2):
230 debug = False
231
232 e = g.next()
233 starts, finishes = e.getTimes()
234 if debug: log.msg("E0", starts, finishes)
235 if finishes == 0:
236 finishes = starts
237 if debug: log.msg("E1 finishes=%s, gap=%s, lET=%s" % \
238 (finishes, idleGap, lastEventTime))
239 if finishes is not None and finishes + idleGap < lastEventTime:
240 if debug: log.msg(" spacer0")
241 yield Spacer(finishes, lastEventTime)
242
243 followingEventStarts = starts
244 if debug: log.msg(" fES0", starts)
245 yield e
246
247 while 1:
248 e = g.next()
249 if not showEvents and isinstance(e, builder.Event):
250 continue
251 starts, finishes = e.getTimes()
252 if debug: log.msg("E2", starts, finishes)
253 if finishes == 0:
254 finishes = starts
255 if finishes is not None and finishes + idleGap < followingEventStarts:
256
257
258
259 if debug:
260 log.msg(" finishes=%s, gap=%s, fES=%s" % \
261 (finishes, idleGap, followingEventStarts))
262 yield Spacer(finishes, followingEventStarts)
263 yield e
264 followingEventStarts = starts
265 if debug: log.msg(" fES1", starts)
266
267
269 pageTitle = "Waterfall Help"
270
274
275 - def content(self, request, cxt):
276 status = self.getStatus(request)
277
278 cxt['show_events_checked'] = request.args.get("show_events", ["false"])[0].lower() == "true"
279 cxt['branches'] = [b for b in request.args.get("branch", []) if b]
280 cxt['failures_only'] = request.args.get("failures_only", ["false"])[0].lower() == "true"
281 cxt['committers'] = [c for c in request.args.get("committer", []) if c]
282
283
284
285 show_builders = request.args.get("show", [])
286 show_builders.extend(request.args.get("builder", []))
287 cxt['show_builders'] = show_builders
288 cxt['all_builders'] = status.getBuilderNames(categories=self.categories)
289
290
291
292 times = [("none", "None"),
293 ("60", "60 seconds"),
294 ("300", "5 minutes"),
295 ("600", "10 minutes"),
296 ]
297 current_reload_time = request.args.get("reload", ["none"])
298 if current_reload_time:
299 current_reload_time = current_reload_time[0]
300 if current_reload_time not in [t[0] for t in times]:
301 times.insert(0, (current_reload_time, current_reload_time) )
302
303 cxt['times'] = times
304 cxt['current_reload_time'] = current_reload_time
305
306 template = request.site.buildbot_service.templates.get_template("waterfallhelp.html")
307 return template.render(**cxt)
308
309
311 "A wrapper around a list of changes to supply the IEventSource interface"
316
318 for change in self.changes:
319 if branches and change.branch not in branches:
320 continue
321 if categories and change.category not in categories:
322 continue
323 if committers and change.author not in committers:
324 continue
325 if minTime and change.when < minTime:
326 continue
327 yield change
328
330 """This builds the main status page, with the waterfall display, and
331 all child pages."""
332
333 - def __init__(self, categories=None, num_events=200, num_events_max=None):
339
340 - def getPageTitle(self, request):
341 status = self.getStatus(request)
342 p = status.getTitle()
343 if p:
344 return "BuildBot: %s" % p
345 else:
346 return "BuildBot"
347
351
353 if "reload" in request.args:
354 try:
355 reload_time = int(request.args["reload"][0])
356 return max(reload_time, 15)
357 except ValueError:
358 pass
359 return None
360
388
389 - def content(self, request, ctx):
390 status = self.getStatus(request)
391 master = request.site.buildbot_service.master
392
393
394
395
396
397 results = {}
398
399
400 changes_d = master.db.changes.getRecentChanges(40)
401 def to_changes(chdicts):
402 return defer.gatherResults([
403 changes.Change.fromChdict(master, chdict)
404 for chdict in chdicts ])
405 changes_d.addCallback(to_changes)
406 def keep_changes(changes):
407 results['changes'] = changes
408 changes_d.addCallback(keep_changes)
409
410
411 allBuilderNames = status.getBuilderNames(categories=self.categories)
412 brstatus_ds = []
413 brcounts = {}
414 def keep_count(statuses, builderName):
415 brcounts[builderName] = len(statuses)
416 for builderName in allBuilderNames:
417 builder_status = status.getBuilder(builderName)
418 d = builder_status.getPendingBuildRequestStatuses()
419 d.addCallback(keep_count, builderName)
420 brstatus_ds.append(d)
421
422
423 d = defer.gatherResults([ changes_d ] + brstatus_ds)
424 def call_content(_):
425 return self.content_with_db_data(results['changes'],
426 brcounts, request, ctx)
427 d.addCallback(call_content)
428 return d
429
430 - def content_with_db_data(self, changes, brcounts, request, ctx):
431 status = self.getStatus(request)
432 ctx['refresh'] = self.get_reload_time(request)
433
434
435
436
437 allBuilderNames = status.getBuilderNames(categories=self.categories)
438 builders = [status.getBuilder(name) for name in allBuilderNames]
439
440
441
442
443
444 showBuilders = request.args.get("show", [])
445 showBuilders.extend(request.args.get("builder", []))
446 if showBuilders:
447 builders = [b for b in builders if b.name in showBuilders]
448
449
450
451
452 showCategories = request.args.get("category", [])
453 if showCategories:
454 builders = [b for b in builders if b.category in showCategories]
455
456
457
458
459 failuresOnly = request.args.get("failures_only", ["false"])[0]
460 if failuresOnly.lower() == "true":
461 builders = [b for b in builders if not self.isSuccess(b)]
462
463 (changeNames, builderNames, timestamps, eventGrid, sourceEvents) = \
464 self.buildGrid(request, builders, changes)
465
466
467 locale_enc = locale.getdefaultlocale()[1]
468 if locale_enc is not None:
469 locale_tz = unicode(time.tzname[time.localtime()[-1]], locale_enc)
470 else:
471 locale_tz = unicode(time.tzname[time.localtime()[-1]])
472 ctx['tz'] = locale_tz
473 ctx['changes_url'] = request.childLink("../changes")
474
475 bn = ctx['builders'] = []
476
477 for name in builderNames:
478 builder = status.getBuilder(name)
479 top_box = ITopBox(builder).getBox(request)
480 current_box = ICurrentBox(builder).getBox(status, brcounts)
481 bn.append({'name': name,
482 'url': request.childLink("../builders/%s" % urllib.quote(name, safe='')),
483 'top': top_box.text,
484 'top_class': top_box.class_,
485 'status': current_box.text,
486 'status_class': current_box.class_,
487 })
488
489 ctx.update(self.phase2(request, changeNames + builderNames, timestamps, eventGrid,
490 sourceEvents))
491
492 def with_args(req, remove_args=[], new_args=[], new_path=None):
493
494 newargs = req.args.copy()
495 for argname in remove_args:
496 newargs[argname] = []
497 if "branch" in newargs:
498 newargs["branch"] = [b for b in newargs["branch"] if b]
499 for k,v in new_args:
500 if k in newargs:
501 newargs[k].append(v)
502 else:
503 newargs[k] = [v]
504 newquery = "&".join(["%s=%s" % (urllib.quote(k), urllib.quote(v))
505 for k in newargs
506 for v in newargs[k]
507 ])
508 if new_path:
509 new_url = new_path
510 elif req.prepath:
511 new_url = req.prepath[-1]
512 else:
513 new_url = ''
514 if newquery:
515 new_url += "?" + newquery
516 return new_url
517
518 if timestamps:
519 bottom = timestamps[-1]
520 ctx['nextpage'] = with_args(request, ["last_time"],
521 [("last_time", str(int(bottom)))])
522
523
524 helpurl = path_to_root(request) + "waterfall/help"
525 ctx['help_url'] = with_args(request, new_path=helpurl)
526
527 if self.get_reload_time(request) is not None:
528 ctx['no_reload_page'] = with_args(request, remove_args=["reload"])
529
530 template = request.site.buildbot_service.templates.get_template("waterfall.html")
531 data = template.render(**ctx)
532 return data
533
534 - def buildGrid(self, request, builders, changes):
535 debug = False
536
537
538 showEvents = False
539 if request.args.get("show_events", ["false"])[0].lower() == "true":
540 showEvents = True
541 filterCategories = request.args.get('category', [])
542 filterBranches = [b for b in request.args.get("branch", []) if b]
543 filterBranches = map_branches(filterBranches)
544 filterCommitters = [c for c in request.args.get("committer", []) if c]
545 maxTime = int(request.args.get("last_time", [util.now()])[0])
546 if "show_time" in request.args:
547 minTime = maxTime - int(request.args["show_time"][0])
548 elif "first_time" in request.args:
549 minTime = int(request.args["first_time"][0])
550 elif filterBranches or filterCommitters:
551 minTime = util.now() - 24 * 60 * 60
552 else:
553 minTime = 0
554 spanLength = 10
555 req_events=int(request.args.get("num_events", [self.num_events])[0])
556 if self.num_events_max and req_events > self.num_events_max:
557 maxPageLen = self.num_events_max
558 else:
559 maxPageLen = req_events
560
561
562
563
564
565 commit_source = ChangeEventSource(changes)
566
567 lastEventTime = util.now()
568 sources = [commit_source] + builders
569 changeNames = ["changes"]
570 builderNames = map(lambda builder: builder.getName(), builders)
571 sourceNames = changeNames + builderNames
572 sourceEvents = []
573 sourceGenerators = []
574
575 def get_event_from(g):
576 try:
577 while True:
578 e = g.next()
579
580
581
582
583
584 if not showEvents and isinstance(e, builder.Event):
585 continue
586 break
587 event = interfaces.IStatusEvent(e)
588 if debug:
589 log.msg("gen %s gave1 %s" % (g, event.getText()))
590 except StopIteration:
591 event = None
592 return event
593
594 for s in sources:
595 gen = insertGaps(s.eventGenerator(filterBranches,
596 filterCategories,
597 filterCommitters,
598 minTime),
599 showEvents,
600 lastEventTime)
601 sourceGenerators.append(gen)
602
603 sourceEvents.append(get_event_from(gen))
604 eventGrid = []
605 timestamps = []
606
607 lastEventTime = 0
608 for e in sourceEvents:
609 if e and e.getTimes()[0] > lastEventTime:
610 lastEventTime = e.getTimes()[0]
611 if lastEventTime == 0:
612 lastEventTime = util.now()
613
614 spanStart = lastEventTime - spanLength
615 debugGather = 0
616
617 while 1:
618 if debugGather: log.msg("checking (%s,]" % spanStart)
619
620
621
622
623
624
625
626 spanEvents = []
627 firstTimestamp = None
628 lastTimestamp = None
629
630 for c in range(len(sourceGenerators)):
631 events = []
632 event = sourceEvents[c]
633 while event and spanStart < event.getTimes()[0]:
634
635
636 if not IBox(event, None):
637 log.msg("BAD EVENT", event, event.getText())
638 assert 0
639 if debug:
640 log.msg("pushing", event.getText(), event)
641 events.append(event)
642 starts, finishes = event.getTimes()
643 firstTimestamp = earlier(firstTimestamp, starts)
644 event = get_event_from(sourceGenerators[c])
645 if debug:
646 log.msg("finished span")
647
648 if event:
649
650 lastTimestamp = later(lastTimestamp,
651 event.getTimes()[0])
652 if debugGather:
653 log.msg(" got %s from %s" % (events, sourceNames[c]))
654 sourceEvents[c] = event
655 spanEvents.append(events)
656
657
658
659
660 if firstTimestamp is not None and firstTimestamp <= maxTime:
661 eventGrid.append(spanEvents)
662 timestamps.append(firstTimestamp)
663
664 if lastTimestamp:
665 spanStart = lastTimestamp - spanLength
666 else:
667
668 break
669 if minTime is not None and lastTimestamp < minTime:
670 break
671
672 if len(timestamps) > maxPageLen:
673 break
674
675
676
677
678
679 if debugGather: log.msg("finished loop")
680 assert(len(timestamps) == len(eventGrid))
681 return (changeNames, builderNames, timestamps, eventGrid, sourceEvents)
682
683 - def phase2(self, request, sourceNames, timestamps, eventGrid,
684 sourceEvents):
685
686 if not timestamps:
687 return dict(grid=[], gridlen=0)
688
689
690 grid = []
691 for i in range(1+len(sourceNames)):
692 grid.append([])
693
694
695
696 lastDate = time.strftime("%d %b %Y",
697 time.localtime(util.now()))
698 for r in range(0, len(timestamps)):
699 chunkstrip = eventGrid[r]
700
701
702 assert(len(chunkstrip) == len(sourceNames))
703 maxRows = reduce(lambda x,y: max(x,y),
704 map(lambda x: len(x), chunkstrip))
705 for i in range(maxRows):
706 if i != maxRows-1:
707 grid[0].append(None)
708 else:
709
710 stuff = []
711
712
713 todayday = time.strftime("%a",
714 time.localtime(timestamps[r]))
715 today = time.strftime("%d %b %Y",
716 time.localtime(timestamps[r]))
717 if today != lastDate:
718 stuff.append(todayday)
719 stuff.append(today)
720 lastDate = today
721 stuff.append(
722 time.strftime("%H:%M:%S",
723 time.localtime(timestamps[r])))
724 grid[0].append(Box(text=stuff, class_="Time",
725 valign="bottom", align="center"))
726
727
728
729 for c in range(0, len(chunkstrip)):
730 block = chunkstrip[c]
731 assert(block != None)
732 for i in range(maxRows - len(block)):
733
734 grid[c+1].append(None)
735 for i in range(len(block)):
736
737 b = IBox(block[i]).getBox(request)
738 b.parms['valign'] = "top"
739 b.parms['align'] = "center"
740 grid[c+1].append(b)
741
742
743 gridlen = len(grid[0])
744 for i in range(len(grid)):
745 strip = grid[i]
746 assert(len(strip) == gridlen)
747 if strip[-1] == None:
748 if sourceEvents[i-1]:
749 filler = IBox(sourceEvents[i-1]).getBox(request)
750 else:
751
752 filler = Box(text=["?"], align="center")
753 strip[-1] = filler
754 strip[-1].parms['rowspan'] = 1
755
756
757
758 noBubble = request.args.get("nobubble",['0'])
759 noBubble = int(noBubble[0])
760 if not noBubble:
761 for col in range(len(grid)):
762 strip = grid[col]
763 if col == 1:
764 for i in range(2, len(strip)+1):
765
766 if strip[-i] == None:
767 next = strip[-i+1]
768 assert(next)
769 if next:
770
771 if next.spacer:
772
773 strip[-i] = next
774 strip[-i].parms['rowspan'] += 1
775 strip[-i+1] = None
776 else:
777
778
779
780 strip[-i] = Box([], rowspan=1,
781 comment="commit bubble")
782 strip[-i].spacer = True
783 else:
784
785
786
787 pass
788 else:
789 for i in range(2, len(strip)+1):
790
791 if strip[-i] == None:
792
793 assert(strip[-i+1] != None)
794 strip[-i] = strip[-i+1]
795 strip[-i].parms['rowspan'] += 1
796 strip[-i+1] = None
797 else:
798 strip[-i].parms['rowspan'] = 1
799
800
801 for i in range(gridlen):
802 for strip in grid:
803 if strip[i]:
804 strip[i] = strip[i].td()
805
806 return dict(grid=grid, gridlen=gridlen, no_bubble=noBubble, time=lastDate)
807