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