1# This file is part of Buildbot. Buildbot is free software: you can 2# redistribute it and/or modify it under the terms of the GNU General Public 3# License as published by the Free Software Foundation, version 2. 4# 5# This program is distributed in the hope that it will be useful, but WITHOUT 6# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 7# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 8# details. 9# 10# You should have received a copy of the GNU General Public License along with 11# this program; if not, write to the Free Software Foundation, Inc., 51 12# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 13# 14# Copyright Buildbot Team Members 15 16 17fromzope.interfaceimportimplements 18fromtwisted.pythonimportlog,components 19importurllib 20 21importtime,locale 22importoperator 23 24frombuildbotimportinterfaces,util 25frombuildbot.statusimportbuilder 26 27frombuildbot.status.web.baseimportBox,HtmlResource,IBox,ICurrentBox, \ 28ITopBox,build_get_class,path_to_build,path_to_step,path_to_root, \ 29map_branches 30 31

70# getState() returns offline, idle, or building 71state,builds=self.original.getState() 72 73# look for upcoming builds. We say the state is "waiting" if the 74# builder is otherwise idle and there is a scheduler which tells us a 75# build will be performed some time in the near future. TODO: this 76# functionality used to be in BuilderStatus.. maybe this code should 77# be merged back into it. 78upcoming=[] 79builderName=self.original.getName() 80forsinstatus.getSchedulers(): 81ifbuilderNameins.listBuilderNames(): 82upcoming.extend(s.getPendingBuildTimes()) 83ifstate=="idle"andupcoming: 84state="waiting" 85 86ifstate=="building": 87text=["building"] 88ifbuilds: 89forbinbuilds: 90eta=b.getETA() 91text.extend(self.formatETA("ETA in",eta)) 92elifstate=="offline": 93text=["offline"] 94elifstate=="idle": 95text=["idle"] 96elifstate=="waiting": 97text=["waiting"] 98else: 99# just in case I add a state and forget to update this100text=[state]101102# TODO: for now, this pending/upcoming stuff is in the "current103# activity" box, but really it should go into a "next activity" row104# instead. The only times it should show up in "current activity" is105# when the builder is otherwise idle.106107# are any builds pending? (waiting for a slave to be free)108pbs=self.original.getPendingBuilds()109ifpbs:110text.append("%d pending"%len(pbs))111fortinupcoming:112eta=t-util.now()113text.extend(self.formatETA("next in",eta))114returnBox(text,class_="Activity "+state)

227debug=False228229e=g.next()230starts,finishes=e.getTimes()231ifdebug:log.msg("E0",starts,finishes)232iffinishes==0:233finishes=starts234ifdebug:log.msg("E1 finishes=%s, gap=%s, lET=%s"% \ 235(finishes,idleGap,lastEventTime))236iffinishesisnotNoneandfinishes+idleGap<lastEventTime:237ifdebug:log.msg(" spacer0")238yieldSpacer(finishes,lastEventTime)239240followingEventStarts=starts241ifdebug:log.msg(" fES0",starts)242yielde243244while1:245e=g.next()246ifnotshowEventsandisinstance(e,builder.Event):247continue248starts,finishes=e.getTimes()249ifdebug:log.msg("E2",starts,finishes)250iffinishes==0:251finishes=starts252iffinishesisnotNoneandfinishes+idleGap<followingEventStarts:253# there is a gap between the end of this event and the beginning254# of the next one. Insert an idle event so the waterfall display255# shows a gap here.256ifdebug:257log.msg(" finishes=%s, gap=%s, fES=%s"% \ 258(finishes,idleGap,followingEventStarts))259yieldSpacer(finishes,followingEventStarts)260yielde261followingEventStarts=starts262ifdebug:log.msg(" fES1",starts)

273status=self.getStatus(request)274275cxt['show_events_checked']=request.args.get("show_events",["false"])[0].lower()=="true"276cxt['branches']=[bforbinrequest.args.get("branch",[])ifb]277cxt['failures_only']=request.args.get("failures_only",["false"])[0].lower()=="true"278cxt['committers']=[cforcinrequest.args.get("committer",[])ifc]279280# this has a set of toggle-buttons to let the user choose the281# builders282show_builders=request.args.get("show",[])283show_builders.extend(request.args.get("builder",[]))284cxt['show_builders']=show_builders285cxt['all_builders']=status.getBuilderNames(categories=self.categories)286287# a couple of radio-button selectors for refresh time will appear288# just after that text289times=[("none","None"),290("60","60 seconds"),291("300","5 minutes"),292("600","10 minutes"),293]294current_reload_time=request.args.get("reload",["none"])295ifcurrent_reload_time:296current_reload_time=current_reload_time[0]297ifcurrent_reload_timenotin[t[0]fortintimes]:298times.insert(0,(current_reload_time,current_reload_time))299300cxt['times']=times301cxt['current_reload_time']=current_reload_time302303template=request.site.buildbot_service.templates.get_template("waterfallhelp.html")304returntemplate.render(**cxt)

340# Helper function to return True if the builder is not failing.341# The function will return false if the current state is "offline",342# the last build was not successful, or if a step from the current343# build(s) failed.344345# Make sure the builder is online.346ifbuilderStatus.getState()[0]=='offline':347returnFalse348349# Look at the last finished build to see if it was success or not.350lastBuild=builderStatus.getLastFinishedBuild()351iflastBuildandlastBuild.getResults()!=builder.SUCCESS:352returnFalse353354# Check all the current builds to see if one step is already355# failing.356currentBuilds=builderStatus.getCurrentBuilds()357ifcurrentBuilds:358forbuildincurrentBuilds:359forstepinbuild.getSteps():360ifstep.getResults()[0]==builder.FAILURE:361returnFalse362363# The last finished build was successful, and all the current builds364# don't have any failed steps.365returnTrue

368status=self.getStatus(request)369ctx['refresh']=self.get_reload_time(request)370371# we start with all Builders available to this Waterfall: this is372# limited by the config-file -time categories= argument, and defaults373# to all defined Builders.374allBuilderNames=status.getBuilderNames(categories=self.categories)375builders=[status.getBuilder(name)fornameinallBuilderNames]376377# but if the URL has one or more builder= arguments (or the old show=378# argument, which is still accepted for backwards compatibility), we379# use that set of builders instead. We still don't show anything380# outside the config-file time set limited by categories=.381showBuilders=request.args.get("show",[])382showBuilders.extend(request.args.get("builder",[]))383ifshowBuilders:384builders=[bforbinbuildersifb.nameinshowBuilders]385386# now, if the URL has one or category= arguments, use them as a387# filter: only show those builders which belong to one of the given388# categories.389showCategories=request.args.get("category",[])390ifshowCategories:391builders=[bforbinbuildersifb.categoryinshowCategories]392393# If the URL has the failures_only=true argument, we remove all the394# builders that are not currently red or won't be turning red at the end395# of their current run.396failuresOnly=request.args.get("failures_only",["false"])[0]397iffailuresOnly.lower()=="true":398builders=[bforbinbuildersifnotself.isSuccess(b)]399400(changeNames,builderNames,timestamps,eventGrid,sourceEvents)= \ 401self.buildGrid(request,builders)402403# start the table: top-header material404locale_enc=locale.getdefaultlocale()[1]405iflocale_encisnotNone:406locale_tz=unicode(time.tzname[time.localtime()[-1]],locale_enc)407else:408locale_tz=unicode(time.tzname[time.localtime()[-1]])409ctx['tz']=locale_tz410ctx['changes_url']=request.childLink("../changes")411412bn=ctx['builders']=[]413414fornameinbuilderNames:415builder=status.getBuilder(name)416top_box=ITopBox(builder).getBox(request)417current_box=ICurrentBox(builder).getBox(status)418bn.append({'name':name,419'url':request.childLink("../builders/%s"%urllib.quote(name,safe='')),420'top':top_box.text,421'top_class':top_box.class_,422'status':current_box.text,423'status_class':current_box.class_,424})425426ctx.update(self.phase2(request,changeNames+builderNames,timestamps,eventGrid,427sourceEvents))428429defwith_args(req,remove_args=[],new_args=[],new_path=None):430# sigh, nevow makes this sort of manipulation easier431newargs=req.args.copy()432forargnameinremove_args:433newargs[argname]=[]434if"branch"innewargs:435newargs["branch"]=[bforbinnewargs["branch"]ifb]436fork,vinnew_args:437ifkinnewargs:438newargs[k].append(v)439else:440newargs[k]=[v]441newquery="&amp;".join(["%s=%s"%(urllib.quote(k),urllib.quote(v))442forkinnewargs443forvinnewargs[k]444])445ifnew_path:446new_url=new_path447elifreq.prepath:448new_url=req.prepath[-1]449else:450new_url=''451ifnewquery:452new_url+="?"+newquery453returnnew_url

472debug=False473# TODO: see if we can use a cached copy474475showEvents=False476ifrequest.args.get("show_events",["false"])[0].lower()=="true":477showEvents=True478filterCategories=request.args.get('category',[])479filterBranches=[bforbinrequest.args.get("branch",[])ifb]480filterBranches=map_branches(filterBranches)481filterCommitters=[cforcinrequest.args.get("committer",[])ifc]482maxTime=int(request.args.get("last_time",[util.now()])[0])483if"show_time"inrequest.args:484minTime=maxTime-int(request.args["show_time"][0])485elif"first_time"inrequest.args:486minTime=int(request.args["first_time"][0])487eliffilterBranchesorfilterCommitters:488minTime=util.now()-24*60*60489else:490minTime=0491spanLength=10# ten-second chunks492req_events=int(request.args.get("num_events",[self.num_events])[0])493ifself.num_events_maxandreq_events>self.num_events_max:494maxPageLen=self.num_events_max495else:496maxPageLen=req_events497498# first step is to walk backwards in time, asking each column499# (commit, all builders) if they have any events there. Build up the500# array of events, and stop when we have a reasonable number.501502commit_source=self.getChangeManager(request)503504lastEventTime=util.now()505sources=[commit_source]+builders506changeNames=["changes"]507builderNames=map(lambdabuilder:builder.getName(),builders)508sourceNames=changeNames+builderNames509sourceEvents=[]510sourceGenerators=[]511512defget_event_from(g):513try:514whileTrue:515e=g.next()516# e might be builder.BuildStepStatus,517# builder.BuildStatus, builder.Event,518# waterfall.Spacer(builder.Event), or changes.Change .519# The showEvents=False flag means we should hide520# builder.Event .521ifnotshowEventsandisinstance(e,builder.Event):522continue523break524event=interfaces.IStatusEvent(e)525ifdebug:526log.msg("gen %s gave1 %s"%(g,event.getText()))527exceptStopIteration:528event=None529returnevent

530531forsinsources:532gen=insertGaps(s.eventGenerator(filterBranches,533filterCategories,534filterCommitters,535minTime),536showEvents,537lastEventTime)538sourceGenerators.append(gen)539# get the first event540sourceEvents.append(get_event_from(gen))541eventGrid=[]542timestamps=[]543544lastEventTime=0545foreinsourceEvents:546ifeande.getTimes()[0]>lastEventTime:547lastEventTime=e.getTimes()[0]548iflastEventTime==0:549lastEventTime=util.now()550551spanStart=lastEventTime-spanLength552debugGather=0553554while1:555ifdebugGather:log.msg("checking (%s,]"%spanStart)556# the tableau of potential events is in sourceEvents[]. The557# window crawls backwards, and we examine one source at a time.558# If the source's top-most event is in the window, is it pushed559# onto the events[] array and the tableau is refilled. This560# continues until the tableau event is not in the window (or is561# missing).562563spanEvents=[]# for all sources, in this span. row of eventGrid564firstTimestamp=None# timestamp of first event in the span565lastTimestamp=None# last pre-span event, for next span566567forcinrange(len(sourceGenerators)):568events=[]# for this source, in this span. cell of eventGrid569event=sourceEvents[c]570whileeventandspanStart<event.getTimes()[0]:571# to look at windows that don't end with the present,572# condition the .append on event.time <= spanFinish573ifnotIBox(event,None):574log.msg("BAD EVENT",event,event.getText())575assert0576ifdebug:577log.msg("pushing",event.getText(),event)578events.append(event)579starts,finishes=event.getTimes()580firstTimestamp=earlier(firstTimestamp,starts)581event=get_event_from(sourceGenerators[c])582ifdebug:583log.msg("finished span")584585ifevent:586# this is the last pre-span event for this source587lastTimestamp=later(lastTimestamp,588event.getTimes()[0])589ifdebugGather:590log.msg(" got %s from %s"%(events,sourceNames[c]))591sourceEvents[c]=event# refill the tableau592spanEvents.append(events)593594# only show events older than maxTime. This makes it possible to595# visit a page that shows what it would be like to scroll off the596# bottom of this one.597iffirstTimestampisnotNoneandfirstTimestamp<=maxTime:598eventGrid.append(spanEvents)599timestamps.append(firstTimestamp)600601iflastTimestamp:602spanStart=lastTimestamp-spanLength603else:604# no more events605break606ifminTimeisnotNoneandlastTimestamp<minTime:607break608609iflen(timestamps)>maxPageLen:610break611612613# now loop614615# loop is finished. now we have eventGrid[] and timestamps[]616ifdebugGather:log.msg("finished loop")617assert(len(timestamps)==len(eventGrid))618return(changeNames,builderNames,timestamps,eventGrid,sourceEvents)619

622623ifnottimestamps:624returndict(grid=[],gridlen=0)625626# first pass: figure out the height of the chunks, populate grid627grid=[]628foriinrange(1+len(sourceNames)):629grid.append([])630# grid is a list of columns, one for the timestamps, and one per631# event source. Each column is exactly the same height. Each element632# of the list is a single <td> box.633lastDate=time.strftime("%d %b %Y",634time.localtime(util.now()))635forrinrange(0,len(timestamps)):636chunkstrip=eventGrid[r]637# chunkstrip is a horizontal strip of event blocks. Each block638# is a vertical list of events, all for the same source.639assert(len(chunkstrip)==len(sourceNames))640maxRows=reduce(lambdax,y:max(x,y),641map(lambdax:len(x),chunkstrip))642foriinrange(maxRows):643ifi!=maxRows-1:644grid[0].append(None)645else:646# timestamp goes at the bottom of the chunk647stuff=[]648# add the date at the beginning (if it is not the same as649# today's date), and each time it changes650todayday=time.strftime("%a",651time.localtime(timestamps[r]))652today=time.strftime("%d %b %Y",653time.localtime(timestamps[r]))654iftoday!=lastDate:655stuff.append(todayday)656stuff.append(today)657lastDate=today658stuff.append(659time.strftime("%H:%M:%S",660time.localtime(timestamps[r])))661grid[0].append(Box(text=stuff,class_="Time",662valign="bottom",align="center"))663664# at this point the timestamp column has been populated with665# maxRows boxes, most None but the last one has the time string666forcinrange(0,len(chunkstrip)):667block=chunkstrip[c]668assert(block!=None)# should be [] instead669foriinrange(maxRows-len(block)):670# fill top of chunk with blank space671grid[c+1].append(None)672foriinrange(len(block)):673# so the events are bottom-justified674b=IBox(block[i]).getBox(request)675b.parms['valign']="top"676b.parms['align']="center"677grid[c+1].append(b)678# now all the other columns have maxRows new boxes too679# populate the last row, if empty680gridlen=len(grid[0])681foriinrange(len(grid)):682strip=grid[i]683assert(len(strip)==gridlen)684ifstrip[-1]==None:685ifsourceEvents[i-1]:686filler=IBox(sourceEvents[i-1]).getBox(request)687else:688# this can happen if you delete part of the build history689filler=Box(text=["?"],align="center")690strip[-1]=filler691strip[-1].parms['rowspan']=1692# second pass: bubble the events upwards to un-occupied locations693# Every square of the grid that has a None in it needs to have694# something else take its place.695noBubble=request.args.get("nobubble",['0'])696noBubble=int(noBubble[0])697ifnotnoBubble:698forcolinrange(len(grid)):699strip=grid[col]700ifcol==1:# changes are handled differently701foriinrange(2,len(strip)+1):702# only merge empty boxes. Don't bubble commit boxes.703ifstrip[-i]==None:704next=strip[-i+1]705assert(next)706ifnext:707#if not next.event:708ifnext.spacer:709# bubble the empty box up710strip[-i]=next711strip[-i].parms['rowspan']+=1712strip[-i+1]=None713else:714# we are above a commit box. Leave it715# be, and turn the current box into an716# empty one717strip[-i]=Box([],rowspan=1,718comment="commit bubble")719strip[-i].spacer=True720else:721# we are above another empty box, which722# somehow wasn't already converted.723# Shouldn't happen724pass725else:726foriinrange(2,len(strip)+1):727# strip[-i] will go from next-to-last back to first728ifstrip[-i]==None:729# bubble previous item up730assert(strip[-i+1]!=None)731strip[-i]=strip[-i+1]732strip[-i].parms['rowspan']+=1733strip[-i+1]=None734else:735strip[-i].parms['rowspan']=1736737# convert to dicts738foriinrange(gridlen):739forstripingrid:740ifstrip[i]:741strip[i]=strip[i].td()742743returndict(grid=grid,gridlen=gridlen,no_bubble=noBubble,time=lastDate)