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
587 if isinstance(e, buildstep.BuildStepStatus):
588
589 if e.isFinished() and e.isHidden():
590 continue
591
592 break
593 event = interfaces.IStatusEvent(e)
594 if debug:
595 log.msg("gen %s gave1 %s" % (g, event.getText()))
596 except StopIteration:
597 event = None
598 return event
599
600 for s in sources:
601 gen = insertGaps(s.eventGenerator(filterBranches,
602 filterCategories,
603 filterCommitters,
604 minTime),
605 showEvents,
606 lastEventTime)
607 sourceGenerators.append(gen)
608
609 sourceEvents.append(get_event_from(gen))
610 eventGrid = []
611 timestamps = []
612
613 lastEventTime = 0
614 for e in sourceEvents:
615 if e and e.getTimes()[0] > lastEventTime:
616 lastEventTime = e.getTimes()[0]
617 if lastEventTime == 0:
618 lastEventTime = util.now()
619
620 spanStart = lastEventTime - spanLength
621 debugGather = 0
622
623 while 1:
624 if debugGather: log.msg("checking (%s,]" % spanStart)
625
626
627
628
629
630
631
632 spanEvents = []
633 firstTimestamp = None
634 lastTimestamp = None
635
636 for c in range(len(sourceGenerators)):
637 events = []
638 event = sourceEvents[c]
639 while event and spanStart < event.getTimes()[0]:
640
641
642 if not IBox(event, None):
643 log.msg("BAD EVENT", event, event.getText())
644 assert 0
645 if debug:
646 log.msg("pushing", event.getText(), event)
647 events.append(event)
648 starts, finishes = event.getTimes()
649 firstTimestamp = earlier(firstTimestamp, starts)
650 event = get_event_from(sourceGenerators[c])
651 if debug:
652 log.msg("finished span")
653
654 if event:
655
656 lastTimestamp = later(lastTimestamp,
657 event.getTimes()[0])
658 if debugGather:
659 log.msg(" got %s from %s" % (events, sourceNames[c]))
660 sourceEvents[c] = event
661 spanEvents.append(events)
662
663
664
665
666 if firstTimestamp is not None and firstTimestamp <= maxTime:
667 eventGrid.append(spanEvents)
668 timestamps.append(firstTimestamp)
669
670 if lastTimestamp:
671 spanStart = lastTimestamp - spanLength
672 else:
673
674 break
675 if minTime is not None and lastTimestamp < minTime:
676 break
677
678 if len(timestamps) > maxPageLen:
679 break
680
681
682
683
684
685 if debugGather: log.msg("finished loop")
686 assert(len(timestamps) == len(eventGrid))
687 return (changeNames, builderNames, timestamps, eventGrid, sourceEvents)
688
689 - def phase2(self, request, sourceNames, timestamps, eventGrid,
690 sourceEvents):
691
692 if not timestamps:
693 return dict(grid=[], gridlen=0)
694
695
696 grid = []
697 for i in range(1+len(sourceNames)):
698 grid.append([])
699
700
701
702 lastDate = time.strftime("%d %b %Y",
703 time.localtime(util.now()))
704 for r in range(0, len(timestamps)):
705 chunkstrip = eventGrid[r]
706
707
708 assert(len(chunkstrip) == len(sourceNames))
709 maxRows = reduce(lambda x,y: max(x,y),
710 map(lambda x: len(x), chunkstrip))
711 for i in range(maxRows):
712 if i != maxRows-1:
713 grid[0].append(None)
714 else:
715
716 stuff = []
717
718
719 todayday = time.strftime("%a",
720 time.localtime(timestamps[r]))
721 today = time.strftime("%d %b %Y",
722 time.localtime(timestamps[r]))
723 if today != lastDate:
724 stuff.append(todayday)
725 stuff.append(today)
726 lastDate = today
727 stuff.append(
728 time.strftime("%H:%M:%S",
729 time.localtime(timestamps[r])))
730 grid[0].append(Box(text=stuff, class_="Time",
731 valign="bottom", align="center"))
732
733
734
735 for c in range(0, len(chunkstrip)):
736 block = chunkstrip[c]
737 assert(block != None)
738 for i in range(maxRows - len(block)):
739
740 grid[c+1].append(None)
741 for i in range(len(block)):
742
743 b = IBox(block[i]).getBox(request)
744 b.parms['valign'] = "top"
745 b.parms['align'] = "center"
746 grid[c+1].append(b)
747
748
749 gridlen = len(grid[0])
750 for i in range(len(grid)):
751 strip = grid[i]
752 assert(len(strip) == gridlen)
753 if strip[-1] == None:
754 if sourceEvents[i-1]:
755 filler = IBox(sourceEvents[i-1]).getBox(request)
756 else:
757
758 filler = Box(text=["?"], align="center")
759 strip[-1] = filler
760 strip[-1].parms['rowspan'] = 1
761
762
763
764 noBubble = request.args.get("nobubble",['0'])
765 noBubble = int(noBubble[0])
766 if not noBubble:
767 for col in range(len(grid)):
768 strip = grid[col]
769 if col == 1:
770 for i in range(2, len(strip)+1):
771
772 if strip[-i] == None:
773 next = strip[-i+1]
774 assert(next)
775 if next:
776
777 if next.spacer:
778
779 strip[-i] = next
780 strip[-i].parms['rowspan'] += 1
781 strip[-i+1] = None
782 else:
783
784
785
786 strip[-i] = Box([], rowspan=1,
787 comment="commit bubble")
788 strip[-i].spacer = True
789 else:
790
791
792
793 pass
794 else:
795 for i in range(2, len(strip)+1):
796
797 if strip[-i] == None:
798
799 assert(strip[-i+1] != None)
800 strip[-i] = strip[-i+1]
801 strip[-i].parms['rowspan'] += 1
802 strip[-i+1] = None
803 else:
804 strip[-i].parms['rowspan'] = 1
805
806
807 for i in range(gridlen):
808 for strip in grid:
809 if strip[i]:
810 strip[i] = strip[i].td()
811
812 return dict(grid=grid, gridlen=gridlen, no_bubble=noBubble, time=lastDate)
813