OLD | NEW |
| (Empty) |
1 from __future__ import generators | |
2 | |
3 import time | |
4 import operator | |
5 import re | |
6 import urllib | |
7 | |
8 from buildbot import util | |
9 from buildbot import version | |
10 from buildbot.status import builder | |
11 from buildbot.status.web.base import HtmlResource | |
12 from buildbot.status.web import console_html as res | |
13 from buildbot.status.web import console_js as js | |
14 | |
15 def isBuildGoingToFail(build): | |
16 """Returns True if one of the step in the running build has failed.""" | |
17 for step in build.getSteps(): | |
18 if step.getResults()[0] == builder.FAILURE: | |
19 return True | |
20 return False | |
21 | |
22 def getInProgressResults(build): | |
23 """Returns build status expectation for an incomplete build.""" | |
24 if not build.isFinished() and isBuildGoingToFail(build): | |
25 return builder.FAILURE | |
26 | |
27 return build.getResults() | |
28 | |
29 def getResultsClass(results, prevResults, inProgress, inProgressResults=None): | |
30 """Given the current and past results, returns the class that will be used | |
31 by the css to display the right color for a box.""" | |
32 | |
33 if inProgress: | |
34 if inProgressResults == builder.FAILURE: | |
35 return "running_failure" | |
36 return "running" | |
37 | |
38 if results is None: | |
39 return "notstarted" | |
40 | |
41 if results == builder.SUCCESS: | |
42 return "success" | |
43 | |
44 if results == builder.FAILURE: | |
45 if not prevResults: | |
46 # This is the bottom box. We don't know if the previous one failed | |
47 # or not. We assume it did not. | |
48 return "failure" | |
49 | |
50 if prevResults != builder.FAILURE: | |
51 # This is a new failure. | |
52 return "failure" | |
53 else: | |
54 # The previous build also failed. | |
55 return "warnings" | |
56 | |
57 # Any other results? Like EXCEPTION? | |
58 return "exception" | |
59 | |
60 cachedBoxes = dict() | |
61 | |
62 class ANYBRANCH: pass # a flag value, used below | |
63 | |
64 class CachedStatusBox: | |
65 def __init__(self, color, title, details, url, tag): | |
66 self.color = color | |
67 self.title = title | |
68 self.details = details | |
69 self.url = url | |
70 self.tag = tag | |
71 | |
72 | |
73 class CacheStatus: | |
74 def __init__(self): | |
75 self.allBoxes = dict() | |
76 self.lastRevisions = dict() | |
77 | |
78 def display(self): | |
79 data = "" | |
80 for builder in self.allBoxes: | |
81 lastRevision = -1 | |
82 try: | |
83 lastRevision = self.lastRevisions[builder] | |
84 except: | |
85 pass | |
86 data += "<br> %s is up to revision %d" % (builder, int(lastRevision)
) | |
87 for revision in self.allBoxes[builder]: | |
88 data += "<br>%s %s %s" % (builder, revision, | |
89 self.allBoxes[builder][revision].color) | |
90 return data | |
91 | |
92 def insert(self, builderName, revision, color, title, details, url, tag): | |
93 box = CachedStatusBox(color, title, details, url, tag) | |
94 try: | |
95 test = self.allBoxes[builderName] | |
96 except: | |
97 self.allBoxes[builderName] = dict() | |
98 | |
99 self.allBoxes[builderName][revision] = box | |
100 | |
101 def get(self, builderName, revision): | |
102 try: | |
103 return self.allBoxes[builderName][revision] | |
104 except: | |
105 return None | |
106 | |
107 def trim(self): | |
108 for builder in self.allBoxes: | |
109 allRevs = [] | |
110 for revision in self.allBoxes[builder]: | |
111 allRevs.append(revision) | |
112 | |
113 if len(allRevs) > 150: | |
114 allRevs.sort() | |
115 deleteCount = len(allRevs) - 150 | |
116 for i in range(0, deleteCount): | |
117 del self.allBoxes[builder][allRevs[i]] | |
118 | |
119 def update(self, builderName, lastRevision): | |
120 currentRevision = 0 | |
121 try: | |
122 currentRevision = self.lastRevisions[builderName] | |
123 except: | |
124 pass | |
125 | |
126 if currentRevision < lastRevision: | |
127 self.lastRevisions[builderName] = lastRevision | |
128 | |
129 def getRevision(self, builderName): | |
130 try: | |
131 return self.lastRevisions[builderName] | |
132 except: | |
133 return None | |
134 | |
135 | |
136 class TemporaryCache: | |
137 def __init__(self): | |
138 self.lastRevisions = dict() | |
139 | |
140 def display(self): | |
141 data = "" | |
142 for builder in self.lastRevisions: | |
143 data += "<br>%s: %s" % (builder, self.lastRevisions[builder]) | |
144 | |
145 return data | |
146 | |
147 def insert(self, builderName, revision): | |
148 currentRevision = 0 | |
149 try: | |
150 currentRevision = self.lastRevisions[builderName] | |
151 except: | |
152 pass | |
153 | |
154 if currentRevision < revision: | |
155 self.lastRevisions[builderName] = revision | |
156 | |
157 def updateGlobalCache(self, global_cache): | |
158 for builder in self.lastRevisions: | |
159 global_cache.update(builder, self.lastRevisions[builder]) | |
160 | |
161 | |
162 class DevRevision: | |
163 """Helper class that contains all the information we need for a revision.""" | |
164 | |
165 def __init__(self, revision, who, comments, date, revlink, when): | |
166 self.revision = revision | |
167 self.comments = comments | |
168 self.who = who | |
169 self.date = date | |
170 self.revlink = revlink | |
171 self.when = when | |
172 | |
173 | |
174 class DevBuild: | |
175 """Helper class that contains all the information we need for a build.""" | |
176 | |
177 def __init__(self, revision, results, inProgressResults, number, isFinished, | |
178 text, eta, details, when): | |
179 self.revision = revision | |
180 self.results = results | |
181 self.inProgressResults = inProgressResults | |
182 self.number = number | |
183 self.isFinished = isFinished | |
184 self.text = text | |
185 self.eta = eta | |
186 self.details = details | |
187 self.when = when | |
188 | |
189 | |
190 class ConsoleStatusResource(HtmlResource): | |
191 """Main console class. It displays a user-oriented status page. | |
192 Every change is a line in the page, and it shows the result of the first | |
193 build with this change for each slave.""" | |
194 | |
195 def __init__(self, allowForce=True, css=None, orderByTime=False): | |
196 HtmlResource.__init__(self) | |
197 | |
198 self.status = None | |
199 self.control = None | |
200 self.changemaster = None | |
201 self.cache = CacheStatus() | |
202 self.initialRevs = None | |
203 | |
204 self.allowForce = allowForce | |
205 self.css = css | |
206 | |
207 if orderByTime: | |
208 self.comparator = TimeRevisionComparator() | |
209 else: | |
210 self.comparator = IntegerRevisionComparator() | |
211 | |
212 def getTitle(self, request): | |
213 status = self.getStatus(request) | |
214 projectName = status.getProjectName() | |
215 if projectName: | |
216 return "BuildBot: %s" % projectName | |
217 else: | |
218 return "BuildBot" | |
219 | |
220 def getChangemaster(self, request): | |
221 return request.site.buildbot_service.parent.change_svc | |
222 | |
223 def head(self, request): | |
224 jsonFormat = request.args.get("json", [False])[0] | |
225 if jsonFormat: | |
226 return "" | |
227 | |
228 # Start by adding all the javascript functions we have. | |
229 head = "<script type='text/javascript'> %s </script>" % js.JAVASCRIPT | |
230 | |
231 reload_time = None | |
232 # Check if there was an arg. Don't let people reload faster than | |
233 # every 15 seconds. 0 means no reload. | |
234 if "reload" in request.args: | |
235 try: | |
236 reload_time = int(request.args["reload"][0]) | |
237 if reload_time != 0: | |
238 reload_time = max(reload_time, 15) | |
239 except ValueError: | |
240 pass | |
241 | |
242 # Append the tag to refresh the page. | |
243 if reload_time is not None and reload_time != 0: | |
244 head += '<meta http-equiv="refresh" content="%d">\n' % reload_time | |
245 return head | |
246 | |
247 | |
248 ## | |
249 ## Data gathering functions | |
250 ## | |
251 | |
252 def getHeadBuild(self, builder): | |
253 """Get the most recent build for the given builder. | |
254 """ | |
255 build = builder.getBuild(-1) | |
256 | |
257 # HACK: Work around #601, the head build may be None if it is | |
258 # locked. | |
259 if build is None: | |
260 build = builder.getBuild(-2) | |
261 | |
262 return build | |
263 | |
264 def fetchChangesFromHistory(self, status, max_depth, max_builds, debugInfo): | |
265 """Look at the history of the builders and try to fetch as many changes | |
266 as possible. We need this when the main source does not contain enough | |
267 sourcestamps. | |
268 | |
269 max_depth defines how many builds we will parse for a given builder. | |
270 max_builds defines how many builds total we want to parse. This is to | |
271 limit the amount of time we spend in this function. | |
272 | |
273 This function is sub-optimal, but the information returned by this | |
274 function is cached, so this function won't be called more than once. | |
275 """ | |
276 | |
277 allChanges = list() | |
278 build_count = 0 | |
279 for builderName in status.getBuilderNames()[:]: | |
280 if build_count > max_builds: | |
281 break | |
282 | |
283 builder = status.getBuilder(builderName) | |
284 build = self.getHeadBuild(builder) | |
285 depth = 0 | |
286 while build and depth < max_depth and build_count < max_builds: | |
287 depth += 1 | |
288 build_count += 1 | |
289 sourcestamp = build.getSourceStamp() | |
290 allChanges.extend(sourcestamp.changes[:]) | |
291 build = build.getPreviousBuild() | |
292 | |
293 debugInfo["source_fetch_len"] = len(allChanges) | |
294 return allChanges | |
295 | |
296 def getAllChanges(self, source, status, debugInfo): | |
297 """Return all the changes we can find at this time. If |source| does not | |
298 not have enough (less than 25), we try to fetch more from the builders | |
299 history.""" | |
300 | |
301 allChanges = list() | |
302 allChanges.extend(source.changes[:]) | |
303 | |
304 debugInfo["source_len"] = len(source.changes) | |
305 | |
306 if len(allChanges) < 25: | |
307 # There is not enough revisions in the source.changes. It happens | |
308 # quite a lot because buildbot mysteriously forget about changes | |
309 # once in a while during restart. | |
310 # Let's try to get more changes from the builders. | |
311 # We check the last 10 builds of all builders, and stop when we | |
312 # are done, or have looked at 100 builds. | |
313 # We do this only once! | |
314 if not self.initialRevs: | |
315 self.initialRevs = self.fetchChangesFromHistory(status, 10, 100, | |
316 debugInfo) | |
317 | |
318 allChanges.extend(self.initialRevs) | |
319 | |
320 # the new changes are not sorted, and can contain duplicates. | |
321 # Sort the list. | |
322 allChanges.sort(lambda a, b: cmp(getattr(a, self.comparator.getSorti
ngKey()), getattr(b, self.comparator.getSortingKey()))) | |
323 | |
324 # Remove the dups | |
325 prevChange = None | |
326 newChanges = [] | |
327 for change in allChanges: | |
328 rev = change.revision | |
329 if not prevChange or rev != prevChange.revision: | |
330 newChanges.append(change) | |
331 prevChange = change | |
332 allChanges = newChanges | |
333 | |
334 return allChanges | |
335 | |
336 def stripRevisions(self, allChanges, numRevs, branch, devName): | |
337 """Returns a subset of changesn from allChanges that matches the query. | |
338 | |
339 allChanges is the list of all changes we know about. | |
340 numRevs is the number of changes we will inspect from allChanges. We | |
341 do not want to inspect all of them or it would be too slow. | |
342 branch is the branch we are interested in. Changes not in this branch | |
343 will be ignored. | |
344 devName is the committer username. Changes that have not been submitted | |
345 by this person will be ignored. | |
346 """ | |
347 | |
348 revisions = [] | |
349 | |
350 if not allChanges: | |
351 return revisions | |
352 | |
353 totalRevs = len(allChanges) | |
354 for i in range(totalRevs-1, totalRevs-numRevs, -1): | |
355 if i < 0: | |
356 break | |
357 change = allChanges[i] | |
358 if branch == ANYBRANCH or branch == change.branch: | |
359 if not devName or change.who in devName: | |
360 rev = DevRevision(change.revision, change.who, | |
361 change.comments, change.getTime(), | |
362 getattr(change, 'revlink', None), | |
363 change.when) | |
364 revisions.append(rev) | |
365 | |
366 return revisions | |
367 | |
368 def getBuildDetails(self, request, builderName, build): | |
369 """Returns an HTML list of failures for a given build.""" | |
370 details = "" | |
371 if build.getLogs(): | |
372 for step in build.getSteps(): | |
373 (result, reason) = step.getResults() | |
374 if result == builder.FAILURE: | |
375 name = step.getName() | |
376 | |
377 # Remove html tags from the error text. | |
378 stripHtml = re.compile(r'<.*?>') | |
379 strippedDetails = stripHtml .sub('', ' '.join(step.getText())) | |
380 | |
381 details += "<li> %s : %s. \n" % (builderName, strippedDetails) | |
382 if step.getLogs(): | |
383 details += "[ " | |
384 for log in step.getLogs(): | |
385 logname = log.getName() | |
386 logurl = request.childLink( | |
387 "../builders/%s/builds/%s/steps/%s/logs/%s" % | |
388 (urllib.quote(builderName), | |
389 build.getNumber(), | |
390 urllib.quote(name), | |
391 urllib.quote(logname))) | |
392 details += "<a href=\"%s\">%s</a> " % (logurl, | |
393 log.getName()) | |
394 details += "]" | |
395 return details | |
396 | |
397 def getBuildsForRevision(self, request, builder, builderName, lastRevision, | |
398 numBuilds, debugInfo): | |
399 """Return the list of all the builds for a given builder that we will | |
400 need to be able to display the console page. We start by the most recent | |
401 build, and we go down until we find a build that was built prior to the | |
402 last change we are interested in.""" | |
403 | |
404 revision = lastRevision | |
405 cachedRevision = self.cache.getRevision(builderName) | |
406 if cachedRevision and cachedRevision > lastRevision: | |
407 revision = cachedRevision | |
408 | |
409 builds = [] | |
410 build = self.getHeadBuild(builder) | |
411 number = 0 | |
412 while build and number < numBuilds: | |
413 debugInfo["builds_scanned"] += 1 | |
414 number += 1 | |
415 | |
416 # Get the last revision in this build. | |
417 # We first try "got_revision", but if it does not work, then | |
418 # we try "revision". | |
419 got_rev = -1 | |
420 try: | |
421 got_rev = build.getProperty("got_revision") | |
422 if not self.comparator.isValidRevision(got_rev): | |
423 got_rev = -1 | |
424 except KeyError: | |
425 pass | |
426 | |
427 try: | |
428 if got_rev == -1: | |
429 got_rev = build.getProperty("revision") | |
430 if not self.comparator.isValidRevision(got_rev): | |
431 got_rev = -1 | |
432 except: | |
433 pass | |
434 | |
435 # We ignore all builds that don't have last revisions. | |
436 # TODO(nsylvain): If the build is over, maybe it was a problem | |
437 # with the update source step. We need to find a way to tell the | |
438 # user that his change might have broken the source update. | |
439 if got_rev and got_rev != -1: | |
440 details = self.getBuildDetails(request, builderName, build) | |
441 devBuild = DevBuild(got_rev, build.getResults(), | |
442 getInProgressResults(build), | |
443 build.getNumber(), | |
444 build.isFinished(), | |
445 build.getText(), | |
446 build.getETA(), | |
447 details, | |
448 build.getTimes()[0]) | |
449 | |
450 builds.append(devBuild) | |
451 | |
452 # Now break if we have enough builds. | |
453 if self.comparator.getSortingKey() == "when": | |
454 current_revision = self.getChangeForBuild( | |
455 builder.getBuild(-1), revision) | |
456 if self.comparator.isRevisionEarlier( | |
457 devBuild, current_revision): | |
458 break | |
459 else: | |
460 if int(got_rev) < int(revision): | |
461 break; | |
462 | |
463 | |
464 build = build.getPreviousBuild() | |
465 | |
466 return builds | |
467 | |
468 def getChangeForBuild(self, build, revision): | |
469 if not build.getChanges(): # Forced build | |
470 devBuild = DevBuild(revision, build.getResults(), | |
471 None, | |
472 build.getNumber(), | |
473 build.isFinished(), | |
474 build.getText(), | |
475 build.getETA(), | |
476 None, | |
477 build.getTimes()[0]) | |
478 | |
479 return devBuild | |
480 | |
481 for change in build.getChanges(): | |
482 if change.revision == revision: | |
483 return change | |
484 | |
485 # No matching change, return the last change in build. | |
486 changes = list(build.getChanges()) | |
487 changes.sort(lambda a, b: cmp(getattr(a, self.comparator.getSortingKey()
), getattr(b, self.comparator.getSortingKey()))) | |
488 return changes[-1] | |
489 | |
490 def getAllBuildsForRevision(self, status, request, lastRevision, numBuilds, | |
491 categories, builders, debugInfo): | |
492 """Returns a dictionnary of builds we need to inspect to be able to | |
493 display the console page. The key is the builder name, and the value is | |
494 an array of build we care about. We also returns a dictionnary of | |
495 builders we care about. The key is it's category. | |
496 | |
497 lastRevision is the last revision we want to display in the page. | |
498 categories is a list of categories to display. It is coming from the | |
499 HTTP GET parameters. | |
500 builders is a list of builders to display. It is coming from the HTTP | |
501 GET parameters. | |
502 """ | |
503 | |
504 allBuilds = dict() | |
505 | |
506 # List of all builders in the dictionnary. | |
507 builderList = dict() | |
508 | |
509 debugInfo["builds_scanned"] = 0 | |
510 # Get all the builders. | |
511 builderNames = status.getBuilderNames()[:] | |
512 for builderName in builderNames: | |
513 builder = status.getBuilder(builderName) | |
514 | |
515 # Make sure we are interested in this builder. | |
516 if categories and builder.category not in categories: | |
517 continue | |
518 if builders and builderName not in builders: | |
519 continue | |
520 | |
521 # We want to display this builder. | |
522 category = builder.category or "default" | |
523 # Strip the category to keep only the text before the first |. | |
524 # This is a hack to support the chromium usecase where they have | |
525 # multiple categories for each slave. We use only the first one. | |
526 # TODO(nsylvain): Create another way to specify "display category" | |
527 # in master.cfg. | |
528 category = category.split('|')[0] | |
529 if not builderList.get(category): | |
530 builderList[category] = [] | |
531 | |
532 # Append this builder to the dictionnary of builders. | |
533 builderList[category].append(builderName) | |
534 # Set the list of builds for this builder. | |
535 allBuilds[builderName] = self.getBuildsForRevision(request, | |
536 builder, | |
537 builderName, | |
538 lastRevision, | |
539 numBuilds, | |
540 debugInfo) | |
541 | |
542 return (builderList, allBuilds) | |
543 | |
544 | |
545 ## | |
546 ## Display functions | |
547 ## | |
548 | |
549 def displayCategories(self, builderList, debugInfo, subs): | |
550 """Display the top category line.""" | |
551 | |
552 data = res.main_line_category_header.substitute(subs) | |
553 count = 0 | |
554 for category in builderList: | |
555 count += len(builderList[category]) | |
556 | |
557 i = 0 | |
558 categories = builderList.keys() | |
559 categories.sort() | |
560 for category in categories: | |
561 # First, we add a flag to say if it's the first or the last one. | |
562 # This is useful is your css is doing rounding at the edge of the | |
563 # tables. | |
564 subs["first"] = "" | |
565 subs["last"] = "" | |
566 if i == 0: | |
567 subs["first"] = "first" | |
568 if i == len(builderList) -1: | |
569 subs["last"] = "last" | |
570 | |
571 # TODO(nsylvain): Another hack to display the category in a pretty | |
572 # way. If the master owner wants to display the categories in a | |
573 # given order, he/she can prepend a number to it. This number won't | |
574 # be shown. | |
575 subs["category"] = category.lstrip('0123456789') | |
576 | |
577 # To be able to align the table correctly, we need to know | |
578 # what percentage of space this category will be taking. This is | |
579 # (#Builders in Category) / (#Builders Total) * 100. | |
580 subs["size"] = (len(builderList[category]) * 100) / count | |
581 data += res.main_line_category_name.substitute(subs) | |
582 i += 1 | |
583 data += res.main_line_category_footer.substitute(subs) | |
584 return data | |
585 | |
586 def displaySlaveLine(self, status, builderList, debugInfo, subs, jsonFormat=
False): | |
587 """Display a line the shows the current status for all the builders we | |
588 care about.""" | |
589 | |
590 data = "" | |
591 json = "" | |
592 | |
593 # Display the first TD (empty) element. | |
594 subs["last"] = "" | |
595 if len(builderList) == 1: | |
596 subs["last"] = "last" | |
597 data += res.main_line_slave_header.substitute(subs) | |
598 | |
599 nbSlaves = 0 | |
600 subs["first"] = "" | |
601 | |
602 # Get the number of builders. | |
603 for category in builderList: | |
604 nbSlaves += len(builderList[category]) | |
605 | |
606 i = 0 | |
607 | |
608 # Get the catefories, and order them alphabetically. | |
609 categories = builderList.keys() | |
610 categories.sort() | |
611 json += '[' | |
612 | |
613 # For each category, we display each builder. | |
614 for category in categories: | |
615 subs["last"] = "" | |
616 | |
617 # If it's the last category, we set the "last" flag. | |
618 if i == len(builderList) - 1: | |
619 subs["last"] = "last" | |
620 | |
621 # This is not the first category, we need to add the spacing we have | |
622 # between 2 categories. | |
623 if i != 0: | |
624 data += res.main_line_slave_section.substitute(subs) | |
625 | |
626 i += 1 | |
627 | |
628 # For each builder in this category, we set the build info and we | |
629 # display the box. | |
630 for builder in builderList[category]: | |
631 subs["color"] = "notstarted" | |
632 subs["title"] = builder | |
633 subs["url"] = "./builders/%s" % urllib.quote(builder) | |
634 state, builds = status.getBuilder(builder).getState() | |
635 # Check if it's offline, if so, the box is purple. | |
636 if state == "offline": | |
637 subs["color"] = "exception" | |
638 else: | |
639 # If not offline, then display the result of the last | |
640 # finished build. | |
641 build = self.getHeadBuild(status.getBuilder(builder)) | |
642 while build and not build.isFinished(): | |
643 build = build.getPreviousBuild() | |
644 | |
645 if build: | |
646 subs["color"] = getResultsClass(build.getResults(), None, | |
647 False) | |
648 | |
649 json += ("{'url': '%s', 'title': '%s', 'color': '%s'," | |
650 " 'name': '%s'}," % (subs["url"], subs["title"], | |
651 subs["color"], | |
652 urllib.quote(builder))) | |
653 | |
654 data += res.main_line_slave_status.substitute(subs) | |
655 | |
656 json += ']' | |
657 data += res.main_line_slave_footer.substitute(subs) | |
658 | |
659 if jsonFormat: | |
660 return json | |
661 return data | |
662 | |
663 def displayStatusLine(self, builderList, allBuilds, revision, tempCache, | |
664 debugInfo, subs, jsonFormat=False): | |
665 """Display the boxes that represent the status of each builder in the | |
666 first build "revision" was in. Returns an HTML list of errors that | |
667 happened during these builds.""" | |
668 | |
669 data = "" | |
670 json = "" | |
671 | |
672 # Display the first TD (empty) element. | |
673 subs["last"] = "" | |
674 if len(builderList) == 1: | |
675 subs["last"] = "last" | |
676 data += res.main_line_status_header.substitute(subs) | |
677 | |
678 details = "" | |
679 nbSlaves = 0 | |
680 subs["first"] = "" | |
681 for category in builderList: | |
682 nbSlaves += len(builderList[category]) | |
683 | |
684 i = 0 | |
685 # Sort the categories. | |
686 categories = builderList.keys() | |
687 categories.sort() | |
688 json += '[' | |
689 | |
690 # Display the boxes by category group. | |
691 for category in categories: | |
692 # Last category? We set the "last" flag. | |
693 subs["last"] = "" | |
694 if i == len(builderList) - 1: | |
695 subs["last"] = "last" | |
696 | |
697 # Not the first category? We add the spacing between 2 categories. | |
698 if i != 0: | |
699 data += res.main_line_status_section.substitute(subs) | |
700 i += 1 | |
701 | |
702 # Display the boxes for each builder in this category. | |
703 for builder in builderList[category]: | |
704 introducedIn = None | |
705 firstNotIn = None | |
706 | |
707 cached_value = self.cache.get(builder, revision.revision) | |
708 if cached_value: | |
709 debugInfo["from_cache"] += 1 | |
710 subs["url"] = cached_value.url | |
711 subs["title"] = cached_value.title | |
712 subs["color"] = cached_value.color | |
713 subs["tag"] = cached_value.tag | |
714 data += res.main_line_status_box.substitute(subs) | |
715 | |
716 json += ("{'url': '%s', 'title': '%s', 'color': '%s'," | |
717 " 'name': '%s'}," % (subs["url"], subs["title"], | |
718 subs["color"], | |
719 urllib.quote(builder))) | |
720 | |
721 # If the box is red, we add the explaination in the details | |
722 # section. | |
723 if cached_value.details and cached_value.color == "failure": | |
724 details += cached_value.details | |
725 | |
726 continue | |
727 | |
728 | |
729 # Find the first build that does not include the revision. | |
730 for build in allBuilds[builder]: | |
731 if self.comparator.isRevisionEarlier(build, revision): | |
732 firstNotIn = build | |
733 break | |
734 else: | |
735 introducedIn = build | |
736 | |
737 # Get the results of the first build with the revision, and the | |
738 # first build that does not include the revision. | |
739 results = None | |
740 inProgressResults = None | |
741 previousResults = None | |
742 if introducedIn: | |
743 results = introducedIn.results | |
744 inProgressResults = introducedIn.inProgressResults | |
745 if firstNotIn: | |
746 previousResults = firstNotIn.results | |
747 | |
748 isRunning = False | |
749 if introducedIn and not introducedIn.isFinished: | |
750 isRunning = True | |
751 | |
752 url = "./waterfall" | |
753 title = builder | |
754 tag = "" | |
755 current_details = None | |
756 if introducedIn: | |
757 current_details = introducedIn.details or "" | |
758 url = "./buildstatus?builder=%s&number=%s" % (urllib.quote(b
uilder), | |
759 introducedIn.n
umber) | |
760 title += " " | |
761 title += urllib.quote(' '.join(introducedIn.text), ' \n\\/:'
) | |
762 | |
763 builderStrip = builder.replace(' ', '') | |
764 builderStrip = builderStrip.replace('(', '') | |
765 builderStrip = builderStrip.replace(')', '') | |
766 builderStrip = builderStrip.replace('.', '') | |
767 tag = "Tag%s%s" % (builderStrip, introducedIn.number) | |
768 | |
769 if isRunning: | |
770 title += ' ETA: %ds' % (introducedIn.eta or 0) | |
771 | |
772 resultsClass = getResultsClass(results, previousResults, isRunni
ng, | |
773 inProgressResults) | |
774 subs["url"] = url | |
775 subs["title"] = title | |
776 subs["color"] = resultsClass | |
777 subs["tag"] = tag | |
778 | |
779 json += ("{'url': '%s', 'title': '%s', 'color': '%s'," | |
780 " 'name': '%s'}," % (url, title, resultsClass, | |
781 urllib.quote(builder))) | |
782 data += res.main_line_status_box.substitute(subs) | |
783 | |
784 # If the box is red, we add the explaination in the details | |
785 # section. | |
786 if current_details and resultsClass == "failure": | |
787 details += current_details | |
788 | |
789 # Add this box to the cache if it's completed so we don't have | |
790 # to compute it again. | |
791 if resultsClass != "running" and \ | |
792 resultsClass != "running_failure" and \ | |
793 resultsClass != "notstarted": | |
794 debugInfo["added_blocks"] += 1 | |
795 self.cache.insert(builder, revision.revision, resultsClass, ti
tle, | |
796 current_details, url, tag) | |
797 tempCache.insert(builder, revision.revision) | |
798 | |
799 json += ']' | |
800 data += res.main_line_status_footer.substitute(subs) | |
801 | |
802 if jsonFormat: | |
803 return (json, details) | |
804 | |
805 return (data, details) | |
806 | |
807 def displayPage(self, request, status, builderList, allBuilds, revisions, | |
808 categories, branch, tempCache, debugInfo, jsonFormat=False): | |
809 """Display the console page.""" | |
810 # Build the main template directory with all the informations we have. | |
811 subs = dict() | |
812 subs["projectUrl"] = status.getProjectURL() or "" | |
813 subs["projectName"] = status.getProjectName() or "" | |
814 safe_branch = branch | |
815 if safe_branch and safe_branch != ANYBRANCH: | |
816 safe_branch = urllib.quote(safe_branch) | |
817 subs["branch"] = safe_branch or 'trunk' | |
818 if categories: | |
819 subs["categories"] = urllib.quote(' '.join(categories)).replace( | |
820 '%20', ' ') | |
821 subs["welcomeUrl"] = self.path_to_root(request) + "index.html" | |
822 subs["version"] = version | |
823 subs["time"] = time.strftime("%a %d %b %Y %H:%M:%S", | |
824 time.localtime(util.now())) | |
825 subs["debugInfo"] = debugInfo | |
826 | |
827 | |
828 # | |
829 # Show the header. | |
830 # | |
831 | |
832 json = "[" | |
833 data = res.top_header.substitute(subs) | |
834 data += res.top_info_name.substitute(subs) | |
835 | |
836 if categories: | |
837 data += res.top_info_categories.substitute(subs) | |
838 | |
839 if branch != ANYBRANCH: | |
840 data += res.top_info_branch.substitute(subs) | |
841 | |
842 data += res.top_info_name_end.substitute(subs) | |
843 # Display the legend. | |
844 data += res.top_legend.substitute(subs) | |
845 | |
846 # Display the personalize box. | |
847 data += res.top_personalize.substitute(subs) | |
848 | |
849 data += res.top_footer.substitute(subs) | |
850 | |
851 | |
852 # | |
853 # Display the main page | |
854 # | |
855 data += res.main_header.substitute(subs) | |
856 | |
857 # "Alt" is set for every other line, to be able to switch the background | |
858 # color. | |
859 subs["alt"] = "Alt" | |
860 subs["first"] = "" | |
861 subs["last"] = "" | |
862 | |
863 # Display the categories if there is more than 1. | |
864 if builderList and len(builderList) > 1: | |
865 dataToAdd = self.displayCategories(builderList, debugInfo, subs) | |
866 data += dataToAdd | |
867 | |
868 # Display the build slaves status. | |
869 if builderList: | |
870 dataToAdd = self.displaySlaveLine(status, builderList, debugInfo, | |
871 subs, jsonFormat) | |
872 data += dataToAdd | |
873 json += dataToAdd + "," | |
874 | |
875 # For each revision we show one line | |
876 for revision in revisions: | |
877 if not subs["alt"]: | |
878 subs["alt"] = "Alt" | |
879 else: | |
880 subs["alt"] = "" | |
881 | |
882 # Fill the dictionnary with these new information | |
883 subs["revision"] = revision.revision | |
884 if revision.revlink: | |
885 subs["revision_link"] = ("<a href=\"%s\">%s</a>" | |
886 % (revision.revlink, | |
887 revision.revision)) | |
888 else: | |
889 subs["revision_link"] = revision.revision | |
890 subs["who"] = revision.who | |
891 subs["date"] = revision.date | |
892 comment = revision.comments or "" | |
893 subs["comments"] = comment.replace('<', '<').replace('>', '>') | |
894 # Re-encode to make sure it doesn't throw an encoding error on the | |
895 # server. | |
896 try: | |
897 comment_quoted = urllib.quote( | |
898 subs["comments"].decode("utf-8", "ignore").encode( | |
899 "ascii", "xmlcharrefreplace")) | |
900 except UnicodeEncodeError: | |
901 # TODO(maruel): Figure out what's happening. | |
902 comment_quoted = urllib.quote(subs["comments"].encode("utf-8")) | |
903 json += ( "{'revision': '%s', 'date': '%s', 'comments': '%s'," | |
904 "'results' : " ) % (subs["revision"], subs["date"], | |
905 comment_quoted) | |
906 | |
907 # Display the revision number and the committer. | |
908 data += res.main_line_info.substitute(subs) | |
909 | |
910 # Display the status for all builders. | |
911 (dataToAdd, details) = self.displayStatusLine(builderList, | |
912 allBuilds, | |
913 revision, | |
914 tempCache, | |
915 debugInfo, | |
916 subs, | |
917 jsonFormat) | |
918 data += dataToAdd | |
919 json += dataToAdd + "}" | |
920 | |
921 # Calculate the td span for the comment and the details. | |
922 subs["span"] = len(builderList) + 2 | |
923 | |
924 # Display the details of the failures, if any. | |
925 if details: | |
926 subs["details"] = details | |
927 data += res.main_line_details.substitute(subs) | |
928 | |
929 # Display the comments for this revision | |
930 data += res.main_line_comments.substitute(subs) | |
931 | |
932 data += res.main_footer.substitute(subs) | |
933 | |
934 # | |
935 # Display the footer of the page. | |
936 # | |
937 debugInfo["load_time"] = time.time() - debugInfo["load_time"] | |
938 data += res.bottom.substitute(subs) | |
939 | |
940 json += "]" | |
941 if jsonFormat: | |
942 return json | |
943 | |
944 return data | |
945 | |
946 def body(self, request): | |
947 "This method builds the main console view display." | |
948 | |
949 # Debug information to display at the end of the page. | |
950 debugInfo = dict() | |
951 debugInfo["load_time"] = time.time() | |
952 | |
953 # get url parameters | |
954 # Categories to show information for. | |
955 categories = request.args.get("category", []) | |
956 # List of all builders to show on the page. | |
957 builders = request.args.get("builder", []) | |
958 # Branch used to filter the changes shown. | |
959 branch = request.args.get("branch", [ANYBRANCH])[0] | |
960 # List of all the committers name to display on the page. | |
961 devName = request.args.get("name", []) | |
962 # json format. | |
963 jsonFormat = request.args.get("json", [False])[0] | |
964 | |
965 | |
966 # and the data we want to render | |
967 status = self.getStatus(request) | |
968 | |
969 projectURL = status.getProjectURL() | |
970 projectName = status.getProjectName() | |
971 | |
972 # Get all revisions we can find. | |
973 source = self.getChangemaster(request) | |
974 allChanges = self.getAllChanges(source, status, debugInfo) | |
975 | |
976 debugInfo["source_all"] = len(allChanges) | |
977 | |
978 # Keep only the revisions we care about. | |
979 # By default we process the last 40 revisions. | |
980 # If a dev name is passed, we look for the changes by this person in the | |
981 # last 160 revisions. | |
982 numRevs = 40 | |
983 if devName: | |
984 numRevs *= 4 | |
985 numBuilds = numRevs | |
986 | |
987 | |
988 revisions = self.stripRevisions(allChanges, numRevs, branch, devName) | |
989 debugInfo["revision_final"] = len(revisions) | |
990 | |
991 # Fetch all the builds for all builders until we get the next build | |
992 # after lastRevision. | |
993 builderList = None | |
994 allBuilds = None | |
995 if revisions: | |
996 lastRevision = revisions[len(revisions)-1].revision | |
997 debugInfo["last_revision"] = lastRevision | |
998 | |
999 (builderList, allBuilds) = self.getAllBuildsForRevision(status, | |
1000 request, | |
1001 lastRevision, | |
1002 numBuilds, | |
1003 categories, | |
1004 builders, | |
1005 debugInfo) | |
1006 | |
1007 tempCache = TemporaryCache() | |
1008 debugInfo["added_blocks"] = 0 | |
1009 debugInfo["from_cache"] = 0 | |
1010 | |
1011 data = "" | |
1012 | |
1013 if request.args.get("display_cache", None): | |
1014 data += "<br>Global Cache" | |
1015 data += self.cache.display() | |
1016 data += "<br>Temporary Cache" | |
1017 data += tempCache.display() | |
1018 | |
1019 if (jsonFormat and int(jsonFormat) == 1): | |
1020 revisions = revisions[0:1] | |
1021 data += self.displayPage(request, status, builderList, allBuilds, | |
1022 revisions, categories, branch, tempCache, | |
1023 debugInfo, jsonFormat) | |
1024 | |
1025 if not devName and branch == ANYBRANCH and not categories and not jsonFo
rmat: | |
1026 tempCache.updateGlobalCache(self.cache) | |
1027 self.cache.trim() | |
1028 | |
1029 return data | |
1030 | |
1031 class RevisionComparator(object): | |
1032 """Used for comparing between revisions, as some | |
1033 VCS use a plain counter for revisions (like SVN) | |
1034 while others use different concepts (see Git). | |
1035 """ | |
1036 | |
1037 # TODO (avivby): Should this be a zope interface? | |
1038 | |
1039 def isRevisionEarlier(self, first_change, second_change): | |
1040 """Used for comparing 2 changes""" | |
1041 raise NotImplementedError | |
1042 | |
1043 def isValidRevision(self, revision): | |
1044 """Checks whether the revision seems like a VCS revision""" | |
1045 raise NotImplementedError | |
1046 | |
1047 def getSortingKey(self): | |
1048 raise NotImplementedError | |
1049 | |
1050 class TimeRevisionComparator(RevisionComparator): | |
1051 def isRevisionEarlier(self, first, second): | |
1052 return first.when < second.when | |
1053 | |
1054 def isValidRevision(self, revision): | |
1055 return True # No general way of determining | |
1056 | |
1057 def getSortingKey(self): | |
1058 return "when" | |
1059 | |
1060 class IntegerRevisionComparator(RevisionComparator): | |
1061 def isRevisionEarlier(self, first, second): | |
1062 return int(first.revision) < int(second.revision) | |
1063 | |
1064 def isValidRevision(self, revision): | |
1065 try: | |
1066 int(revision) | |
1067 return True | |
1068 except: | |
1069 return False | |
1070 | |
1071 def getSortingKey(self): | |
1072 return "revision" | |
OLD | NEW |