Package buildbot :: Package status :: Package web :: Module grid
[frames] | no frames]

Source Code for Module buildbot.status.web.grid

  1  from __future__ import generators 
  2   
  3  import sys, time, os.path 
  4  import urllib 
  5   
  6  from twisted.web import html, resource 
  7   
  8  from buildbot import util 
  9  from buildbot import version 
 10  from buildbot.status.web.base import HtmlResource 
 11  #from buildbot.status.web.base import Box, HtmlResource, IBox, ICurrentBox, \ 
 12  #     ITopBox, td, build_get_class, path_to_build, path_to_step, map_branches 
 13  from buildbot.status.web.base import build_get_class 
 14   
 15  # set grid_css to the full pathname of the css file 
 16  if hasattr(sys, "frozen"): 
 17      # all 'data' files are in the directory of our executable 
 18      here = os.path.dirname(sys.executable) 
 19      grid_css = os.path.abspath(os.path.join(here, "grid.css")) 
 20  else: 
 21      # running from source; look for a sibling to __file__ 
 22      up = os.path.dirname 
 23      grid_css = os.path.abspath(os.path.join(up(__file__), "grid.css")) 
 24   
25 -class ANYBRANCH: pass # a flag value, used below
26
27 -class GridStatusMixin(object):
28 - def getTitle(self, request):
29 status = self.getStatus(request) 30 p = status.getProjectName() 31 if p: 32 return "BuildBot: %s" % p 33 else: 34 return "BuildBot"
35
36 - def getChangemaster(self, request):
37 # TODO: this wants to go away, access it through IStatus 38 return request.site.buildbot_service.getChangeSvc()
39 40 # handle reloads through an http header 41 # TODO: send this as a real header, rather than a tag
42 - def get_reload_time(self, request):
43 if "reload" in request.args: 44 try: 45 reload_time = int(request.args["reload"][0]) 46 return max(reload_time, 15) 47 except ValueError: 48 pass 49 return None
50
51 - def head(self, request):
52 head = '' 53 reload_time = self.get_reload_time(request) 54 if reload_time is not None: 55 head += '<meta http-equiv="refresh" content="%d">\n' % reload_time 56 return head
57 58 # def setBuildmaster(self, buildmaster): 59 # self.status = buildmaster.getStatus() 60 # if self.allowForce: 61 # self.control = interfaces.IControl(buildmaster) 62 # else: 63 # self.control = None 64 # self.changemaster = buildmaster.change_svc 65 # 66 # # try to set the page title 67 # p = self.status.getProjectName() 68 # if p: 69 # self.title = "BuildBot: %s" % p 70 #
71 - def build_td(self, request, build):
72 if not build: 73 return '<td class="build">&nbsp;</td>\n' 74 75 if build.isFinished(): 76 # get the text and annotate the first line with a link 77 text = build.getText() 78 if not text: text = [ "(no information)" ] 79 if text == [ "build", "successful" ]: text = [ "OK" ] 80 else: 81 text = [ 'building' ] 82 83 name = build.getBuilder().getName() 84 number = build.getNumber() 85 url = "builders/%s/builds/%d" % (name, number) 86 text[0] = '<a href="%s">%s</a>' % (url, text[0]) 87 text = '<br />\n'.join(text) 88 class_ = build_get_class(build) 89 90 return '<td class="build %s">%s</td>\n' % (class_, text)
91
92 - def builder_td(self, request, builder):
93 state, builds = builder.getState() 94 95 # look for upcoming builds. We say the state is "waiting" if the 96 # builder is otherwise idle and there is a scheduler which tells us a 97 # build will be performed some time in the near future. TODO: this 98 # functionality used to be in BuilderStatus.. maybe this code should 99 # be merged back into it. 100 upcoming = [] 101 builderName = builder.getName() 102 for s in self.getStatus(request).getSchedulers(): 103 if builderName in s.listBuilderNames(): 104 upcoming.extend(s.getPendingBuildTimes()) 105 if state == "idle" and upcoming: 106 state = "waiting" 107 108 # TODO: for now, this pending/upcoming stuff is in the "current 109 # activity" box, but really it should go into a "next activity" row 110 # instead. The only times it should show up in "current activity" is 111 # when the builder is otherwise idle. 112 113 # are any builds pending? (waiting for a slave to be free) 114 url = 'builders/%s/' % urllib.quote(builder.getName(), safe='') 115 text = '<a href="%s">%s</a>' % (url, builder.getName()) 116 pbs = builder.getPendingBuilds() 117 if state != 'idle' or pbs: 118 if pbs: 119 text += "<br />(%s with %d pending)" % (state, len(pbs)) 120 else: 121 text += "<br />(%s)" % state 122 123 return '<td valign="center" class="builder %s">%s</td>\n' % \ 124 (state, text)
125
126 - def stamp_td(self, stamp):
127 text = stamp.getText() 128 return '<td valign="bottom" class="sourcestamp">%s</td>\n' % \ 129 "<br />".join(text)
130
131 - def getSourceStampKey(self, ss):
132 """Given two source stamps, we want to assign them to the same row if 133 they are the same version of code, even if they differ in minor detail. 134 135 This function returns an appropriate comparison key for that. 136 """ 137 return (ss.branch, ss.revision, ss.patch)
138
139 - def getRecentSourcestamps(self, status, numBuilds, categories, branch):
140 """ 141 get a list of the most recent NUMBUILDS SourceStamp tuples, sorted 142 by the earliest start we've seen for them 143 """ 144 # TODO: use baseweb's getLastNBuilds? 145 sourcestamps = { } # { ss-tuple : earliest time } 146 for bn in status.getBuilderNames(): 147 builder = status.getBuilder(bn) 148 if categories and builder.category not in categories: 149 continue 150 build = builder.getBuild(-1) 151 while build: 152 ss = build.getSourceStamp(absolute=True) 153 start = build.getTimes()[0] 154 build = build.getPreviousBuild() 155 156 # skip un-started builds 157 if not start: continue 158 159 # skip non-matching branches 160 if branch != ANYBRANCH and ss.branch != branch: continue 161 162 key= self.getSourceStampKey(ss) 163 if key not in sourcestamps or sourcestamps[key][1] > start: 164 sourcestamps[key] = (ss, start) 165 166 # now sort those and take the NUMBUILDS most recent 167 sourcestamps = sourcestamps.values() 168 sourcestamps.sort(lambda x, y: cmp(x[1], y[1])) 169 sourcestamps = map(lambda tup : tup[0], sourcestamps) 170 sourcestamps = sourcestamps[-numBuilds:] 171 172 return sourcestamps
173
174 -class GridStatusResource(HtmlResource, GridStatusMixin):
175 # TODO: docs 176 status = None 177 control = None 178 changemaster = None 179
180 - def __init__(self, allowForce=True, css=None):
181 HtmlResource.__init__(self) 182 183 self.allowForce = allowForce 184 self.css = css or grid_css
185 186
187 - def body(self, request):
188 """This method builds the regular grid display. 189 That is, build stamps across the top, build hosts down the left side 190 """ 191 192 # get url parameters 193 numBuilds = int(request.args.get("width", [5])[0]) 194 categories = request.args.get("category", []) 195 branch = request.args.get("branch", [ANYBRANCH])[0] 196 if branch == 'trunk': branch = None 197 198 # and the data we want to render 199 status = self.getStatus(request) 200 stamps = self.getRecentSourcestamps(status, numBuilds, categories, branch) 201 202 projectURL = status.getProjectURL() 203 projectName = status.getProjectName() 204 205 data = '<table class="Grid" border="0" cellspacing="0">\n' 206 data += '<tr>\n' 207 data += '<td class="title"><a href="%s">%s</a>' % (projectURL, projectName) 208 if categories: 209 html_categories = map(html.escape, categories) 210 if len(categories) > 1: 211 data += '\n<br /><b>Categories:</b><br/>%s' % ('<br/>'.join(html_categories)) 212 else: 213 data += '\n<br /><b>Category:</b> %s' % html_categories[0] 214 if branch != ANYBRANCH: 215 data += '\n<br /><b>Branch:</b> %s' % (html.escape(branch or 'trunk')) 216 data += '</td>\n' 217 for stamp in stamps: 218 data += self.stamp_td(stamp) 219 data += '</tr>\n' 220 221 sortedBuilderNames = status.getBuilderNames()[:] 222 sortedBuilderNames.sort() 223 for bn in sortedBuilderNames: 224 builds = [None] * len(stamps) 225 226 builder = status.getBuilder(bn) 227 if categories and builder.category not in categories: 228 continue 229 230 build = builder.getBuild(-1) 231 while build and None in builds: 232 ss = build.getSourceStamp(absolute=True) 233 key= self.getSourceStampKey(ss) 234 for i in range(len(stamps)): 235 if key == self.getSourceStampKey(stamps[i]) and builds[i] is None: 236 builds[i] = build 237 build = build.getPreviousBuild() 238 239 data += '<tr>\n' 240 data += self.builder_td(request, builder) 241 for build in builds: 242 data += self.build_td(request, build) 243 data += '</tr>\n' 244 245 data += '</table>\n' 246 247 data += self.footer(status, request) 248 return data
249
250 -class TransposedGridStatusResource(HtmlResource, GridStatusMixin):
251 # TODO: docs 252 status = None 253 control = None 254 changemaster = None 255
256 - def __init__(self, allowForce=True, css=None):
257 HtmlResource.__init__(self) 258 259 self.allowForce = allowForce 260 self.css = css or grid_css
261 262
263 - def body(self, request):
264 """This method builds the transposed grid display. 265 That is, build hosts across the top, ebuild stamps down the left side 266 """ 267 268 # get url parameters 269 numBuilds = int(request.args.get("length", [5])[0]) 270 categories = request.args.get("category", []) 271 branch = request.args.get("branch", [ANYBRANCH])[0] 272 if branch == 'trunk': branch = None 273 274 # and the data we want to render 275 status = self.getStatus(request) 276 stamps = self.getRecentSourcestamps(status, numBuilds, categories, branch) 277 278 projectURL = status.getProjectURL() 279 projectName = status.getProjectName() 280 281 data = '<table class="Grid" border="0" cellspacing="0">\n' 282 data += '<tr>\n' 283 data += '<td class="title"><a href="%s">%s</a>' % (projectURL, projectName) 284 if categories: 285 html_categories = map(html.escape, categories) 286 if len(categories) > 1: 287 data += '\n<br /><b>Categories:</b><br/>%s' % ('<br/>'.join(html_categories)) 288 else: 289 data += '\n<br /><b>Category:</b> %s' % html_categories[0] 290 if branch != ANYBRANCH: 291 data += '\n<br /><b>Branch:</b> %s' % (html.escape(branch or 'trunk')) 292 data += '</td>\n' 293 294 sortedBuilderNames = status.getBuilderNames()[:] 295 sortedBuilderNames.sort() 296 297 builder_builds = [] 298 299 for bn in sortedBuilderNames: 300 builds = [None] * len(stamps) 301 302 builder = status.getBuilder(bn) 303 if categories and builder.category not in categories: 304 continue 305 306 build = builder.getBuild(-1) 307 while build and None in builds: 308 ss = build.getSourceStamp(absolute=True) 309 key = self.getSourceStampKey(ss) 310 for i in range(len(stamps)): 311 if key == self.getSourceStampKey(stamps[i]) and builds[i] is None: 312 builds[i] = build 313 build = build.getPreviousBuild() 314 315 data += self.builder_td(request, builder) 316 builder_builds.append(builds) 317 318 data += '</tr>\n' 319 320 for i in range(len(stamps)): 321 data += '<tr>\n' 322 data += self.stamp_td(stamps[i]) 323 for builds in builder_builds: 324 data += self.build_td(request, builds[i]) 325 data += '</tr>\n' 326 327 data += '</table>\n' 328 329 data += self.footer(status, request) 330 return data
331