| OLD | NEW |
| (Empty) |
| 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"> </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, branc
h) | |
| 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, projectNa
me) | |
| 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, branc
h) | |
| 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, projectNa
me) | |
| 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 | |
| OLD | NEW |