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 |