def content(self, request, ctx): """Display a build in the same format as the waterfall page. The HTTP GET parameters are the builder name and the build number.""" status = self.getStatus(request) request.setHeader('Cache-Control', 'no-cache') # Get the parameters. name = request.args.get("builder", [None])[0] number = request.args.get("number", [None])[0] if not name or not number: return "builder and number parameter missing" number = int(number) # Check if the builder in parameter exists. try: builder = status.getBuilder(name) except: return "unknown builder" # Check if the build in parameter exists. build = builder.getBuild(int(number)) if not build: return "unknown build %s" % number rows = ctx['rows'] = [] # Display each step, starting by the last one. for i in range(len(build.getSteps()) - 1, -1, -1): step = build.getSteps()[i] if step.isStarted() and step.getText(): rows.append(IBox(step).getBox(request).td(align="center")) # Display the bottom box with the build number in it. ctx['build'] = IBox(build).getBox(request).td(align="center") template = request.site.buildbot_service.templates.get_template( "buildstatus.html") data = template.render(**ctx) # We want all links to display in a new tab/window instead of in the # current one. # TODO: Move to template data = data.replace('<a ', '<a target="_blank" ') return data
def body(self, request): """Display a build in the same format as the waterfall page. The HTTP GET parameters are the builder name and the build number.""" status = self.getStatus(request) data = "" # Get the parameters. name = request.args.get("builder", [None])[0] number = request.args.get("number", [None])[0] if not name or not number: return "builder and number parameter missing" number = int(number) # Main table for the build status. data += '<table>\n' # Check if the builder in parameter exists. try: builder = status.getBuilder(name) except: return "unknown builder" # Check if the build in parameter exists. build = builder.getBuild(int(number)) if not build: return "unknown build %s" % number # Display each step, starting by the last one. for i in range(len(build.getSteps()) - 1, -1, -1): step = build.getSteps()[i] if step.isStarted() and step.getText(): data += " <tr>\n" data += IBox(step).getBox(request).td(align="center") data += " </tr>\n" # Display the bottom box with the build number in it. data += "<tr>" data += IBox(build).getBox(request).td(align="center") data += "</tr></table>\n" # We want all links to display in a new tab/window instead of in the # current one. data = data.replace('<a ', '<a target="_blank"') return data
def phase1(self, request, sourceNames, timestamps, eventGrid, sourceEvents): # phase1 rendering: table, but boxes do not overlap data = "" if not timestamps: return data lastDate = None for r in range(0, len(timestamps)): chunkstrip = eventGrid[r] # chunkstrip is a horizontal strip of event blocks. Each block # is a vertical list of events, all for the same source. assert (len(chunkstrip) == len(sourceNames)) maxRows = reduce(lambda x, y: max(x, y), map(lambda x: len(x), chunkstrip)) for i in range(maxRows): data += " <tr>\n" if i == 0: stuff = [] # add the date at the beginning, and each time it changes today = time.strftime("<b>%d %b %Y</b>", time.localtime(timestamps[r])) todayday = time.strftime("<b>%a</b>", time.localtime(timestamps[r])) if today != lastDate: stuff.append(todayday) stuff.append(today) lastDate = today stuff.append( time.strftime("%H:%M:%S", time.localtime(timestamps[r]))) data += td(stuff, valign="bottom", align="center", rowspan=maxRows, class_="Time") for c in range(0, len(chunkstrip)): block = chunkstrip[c] assert (block != None) # should be [] instead # bottom-justify offset = maxRows - len(block) if i < offset: data += td("") else: e = block[i - offset] box = IBox(e).getBox(request) box.parms["show_idle"] = 1 data += box.td(valign="top", align="center") data += " </tr>\n" return data
def GenBox(item): """Generates a box for one build step.""" # Fix the url root. box_text = IBox(item).getBox(request).td(align='center').replace( 'href="builders/', 'href="' + waterfall_url + 'builders/') # Convert CSS classes to inline style. match = re.search(r"class=\"([^\"]*)\"", box_text) if match: css_class_text = match.group(1) css_classes = css_class_text.split() not_found = [c for c in css_classes if c not in styles] css_classes = [c for c in css_classes if c in styles] if len(not_found): log.msg( 'CSS classes couldn\'t be converted in inline style in ' 'email: %s' % str(not_found)) inline_styles = ' '.join([styles[c] for c in css_classes]) box_text = box_text.replace('class="%s"' % css_class_text, 'style="%s"' % inline_styles) else: log.msg('Couldn\'t find the class attribute') return '<tr>%s</tr>\n' % box_text
def phase2(self, request, sourceNames, timestamps, eventGrid, sourceEvents): data = "" if not timestamps: return data # first pass: figure out the height of the chunks, populate grid grid = [] for i in range(1 + len(sourceNames)): grid.append([]) # grid is a list of columns, one for the timestamps, and one per # event source. Each column is exactly the same height. Each element # of the list is a single <td> box. lastDate = time.strftime("<b>%d %b %Y</b>", time.localtime(util.now())) for r in range(0, len(timestamps)): chunkstrip = eventGrid[r] # chunkstrip is a horizontal strip of event blocks. Each block # is a vertical list of events, all for the same source. assert (len(chunkstrip) == len(sourceNames)) maxRows = reduce(lambda x, y: max(x, y), map(lambda x: len(x), chunkstrip)) for i in range(maxRows): if i != maxRows - 1: grid[0].append(None) else: # timestamp goes at the bottom of the chunk stuff = [] # add the date at the beginning (if it is not the same as # today's date), and each time it changes todayday = time.strftime("<b>%a</b>", time.localtime(timestamps[r])) today = time.strftime("<b>%d %b %Y</b>", time.localtime(timestamps[r])) if today != lastDate: stuff.append(todayday) stuff.append(today) lastDate = today stuff.append( time.strftime("%H:%M:%S", time.localtime(timestamps[r]))) grid[0].append( Box(text=stuff, class_="Time", valign="bottom", align="center")) # at this point the timestamp column has been populated with # maxRows boxes, most None but the last one has the time string for c in range(0, len(chunkstrip)): block = chunkstrip[c] assert (block != None) # should be [] instead for i in range(maxRows - len(block)): # fill top of chunk with blank space grid[c + 1].append(None) for i in range(len(block)): # so the events are bottom-justified b = IBox(block[i]).getBox(request) b.parms['valign'] = "top" b.parms['align'] = "center" grid[c + 1].append(b) # now all the other columns have maxRows new boxes too # populate the last row, if empty gridlen = len(grid[0]) for i in range(len(grid)): strip = grid[i] assert (len(strip) == gridlen) if strip[-1] == None: if sourceEvents[i - 1]: filler = IBox(sourceEvents[i - 1]).getBox(request) else: # this can happen if you delete part of the build history filler = Box(text=["?"], align="center") strip[-1] = filler strip[-1].parms['rowspan'] = 1 # second pass: bubble the events upwards to un-occupied locations # Every square of the grid that has a None in it needs to have # something else take its place. noBubble = request.args.get("nobubble", ['0']) noBubble = int(noBubble[0]) if not noBubble: for col in range(len(grid)): strip = grid[col] if col == 1: # changes are handled differently for i in range(2, len(strip) + 1): # only merge empty boxes. Don't bubble commit boxes. if strip[-i] == None: next = strip[-i + 1] assert (next) if next: #if not next.event: if next.spacer: # bubble the empty box up strip[-i] = next strip[-i].parms['rowspan'] += 1 strip[-i + 1] = None else: # we are above a commit box. Leave it # be, and turn the current box into an # empty one strip[-i] = Box([], rowspan=1, comment="commit bubble") strip[-i].spacer = True else: # we are above another empty box, which # somehow wasn't already converted. # Shouldn't happen pass else: for i in range(2, len(strip) + 1): # strip[-i] will go from next-to-last back to first if strip[-i] == None: # bubble previous item up assert (strip[-i + 1] != None) strip[-i] = strip[-i + 1] strip[-i].parms['rowspan'] += 1 strip[-i + 1] = None else: strip[-i].parms['rowspan'] = 1 # third pass: render the HTML table for i in range(gridlen): data += " <tr>\n" for strip in grid: b = strip[i] if b: data += b.td() else: if noBubble: data += td([]) # Nones are left empty, rowspan should make it all fit data += " </tr>\n" return data
def buildGrid(self, request, builders): debug = False # TODO: see if we can use a cached copy showEvents = False if request.args.get("show_events", ["true"])[0].lower() == "true": showEvents = True filterBranches = [b for b in request.args.get("branch", []) if b] filterBranches = map_branches(filterBranches) maxTime = int(request.args.get("last_time", [util.now()])[0]) if "show_time" in request.args: minTime = maxTime - int(request.args["show_time"][0]) elif "first_time" in request.args: minTime = int(request.args["first_time"][0]) else: minTime = None spanLength = 10 # ten-second chunks maxPageLen = int(request.args.get("num_events", [200])[0]) # first step is to walk backwards in time, asking each column # (commit, all builders) if they have any events there. Build up the # array of events, and stop when we have a reasonable number. commit_source = self.getChangemaster(request) lastEventTime = util.now() sources = [commit_source] + builders changeNames = ["changes"] builderNames = map(lambda builder: builder.getName(), builders) sourceNames = changeNames + builderNames sourceEvents = [] sourceGenerators = [] def get_event_from(g): try: while True: e = g.next() # e might be builder.BuildStepStatus, # builder.BuildStatus, builder.Event, # waterfall.Spacer(builder.Event), or changes.Change . # The showEvents=False flag means we should hide # builder.Event . if not showEvents and isinstance(e, builder.Event): continue break event = interfaces.IStatusEvent(e) if debug: log.msg("gen %s gave1 %s" % (g, event.getText())) except StopIteration: event = None return event for s in sources: gen = insertGaps(s.eventGenerator(filterBranches), lastEventTime) sourceGenerators.append(gen) # get the first event sourceEvents.append(get_event_from(gen)) eventGrid = [] timestamps = [] lastEventTime = 0 for e in sourceEvents: if e and e.getTimes()[0] > lastEventTime: lastEventTime = e.getTimes()[0] if lastEventTime == 0: lastEventTime = util.now() spanStart = lastEventTime - spanLength debugGather = 0 while 1: if debugGather: log.msg("checking (%s,]" % spanStart) # the tableau of potential events is in sourceEvents[]. The # window crawls backwards, and we examine one source at a time. # If the source's top-most event is in the window, is it pushed # onto the events[] array and the tableau is refilled. This # continues until the tableau event is not in the window (or is # missing). spanEvents = [] # for all sources, in this span. row of eventGrid firstTimestamp = None # timestamp of first event in the span lastTimestamp = None # last pre-span event, for next span for c in range(len(sourceGenerators)): events = [] # for this source, in this span. cell of eventGrid event = sourceEvents[c] while event and spanStart < event.getTimes()[0]: # to look at windows that don't end with the present, # condition the .append on event.time <= spanFinish if not IBox(event, None): log.msg("BAD EVENT", event, event.getText()) assert 0 if debug: log.msg("pushing", event.getText(), event) events.append(event) starts, finishes = event.getTimes() firstTimestamp = util.earlier(firstTimestamp, starts) event = get_event_from(sourceGenerators[c]) if debug: log.msg("finished span") if event: # this is the last pre-span event for this source lastTimestamp = util.later(lastTimestamp, event.getTimes()[0]) if debugGather: log.msg(" got %s from %s" % (events, sourceNames[c])) sourceEvents[c] = event # refill the tableau spanEvents.append(events) # only show events older than maxTime. This makes it possible to # visit a page that shows what it would be like to scroll off the # bottom of this one. if firstTimestamp is not None and firstTimestamp <= maxTime: eventGrid.append(spanEvents) timestamps.append(firstTimestamp) if lastTimestamp: spanStart = lastTimestamp - spanLength else: # no more events break if minTime is not None and lastTimestamp < minTime: break if len(timestamps) > maxPageLen: break # now loop # loop is finished. now we have eventGrid[] and timestamps[] if debugGather: log.msg("finished loop") assert (len(timestamps) == len(eventGrid)) return (changeNames, builderNames, timestamps, eventGrid, sourceEvents)