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.status import builder
10 from buildbot.status.web.base import HtmlResource
11
13
15 """Given the current and past results, return the class that will be used
16 by the css to display the right color for a box."""
17
18 if inProgress:
19 return "running"
20
21 if results is None:
22 return "notstarted"
23
24 if results == builder.SUCCESS:
25 return "success"
26
27 if results == builder.WARNINGS:
28 return "warnings"
29
30 if results == builder.FAILURE:
31 if not prevResults:
32
33
34 return "failure"
35
36 if prevResults != builder.FAILURE:
37
38 return "failure"
39 else:
40
41 return "warnings"
42
43
44 return "exception"
45
47
49 """Helper class that contains all the information we need for a revision."""
50
60
61
63 """Helper class that contains all the information we need for a build."""
64
65 - def __init__(self, revision, build, details):
75
76
78 """Main console class. It displays a user-oriented status page.
79 Every change is a line in the page, and it shows the result of the first
80 build with this change for each slave."""
81
91
99
102
103
104
105
106
118
119 - def fetchChangesFromHistory(self, status, max_depth, max_builds, debugInfo):
120 """Look at the history of the builders and try to fetch as many changes
121 as possible. We need this when the main source does not contain enough
122 sourcestamps.
123
124 max_depth defines how many builds we will parse for a given builder.
125 max_builds defines how many builds total we want to parse. This is to
126 limit the amount of time we spend in this function.
127
128 This function is sub-optimal, but the information returned by this
129 function is cached, so this function won't be called more than once.
130 """
131
132 allChanges = list()
133 build_count = 0
134 for builderName in status.getBuilderNames()[:]:
135 if build_count > max_builds:
136 break
137
138 builder = status.getBuilder(builderName)
139 build = self.getHeadBuild(builder)
140 depth = 0
141 while build and depth < max_depth and build_count < max_builds:
142 depth += 1
143 build_count += 1
144 sourcestamp = build.getSourceStamp()
145 allChanges.extend(sourcestamp.changes[:])
146 build = build.getPreviousBuild()
147
148 debugInfo["source_fetch_len"] = len(allChanges)
149 return allChanges
150
152 """Return all the changes we can find at this time. If |source| does not
153 not have enough (less than 25), we try to fetch more from the builders
154 history."""
155
156 g = source.eventGenerator()
157 allChanges = []
158 while len(allChanges) < 25:
159 try:
160 c = g.next()
161 except StopIteration:
162 break
163 allChanges.append(c)
164
165 allChanges.sort(key=self.comparator.getSortingKey())
166
167
168 prevChange = None
169 newChanges = []
170 for change in allChanges:
171 rev = change.revision
172 if not prevChange or rev != prevChange.revision:
173 newChanges.append(change)
174 prevChange = change
175 allChanges = newChanges
176
177 return allChanges
178
180 """Returns an HTML list of failures for a given build."""
181 details = {}
182 if not build.getLogs():
183 return details
184
185 for step in build.getSteps():
186 (result, reason) = step.getResults()
187 if result == builder.FAILURE:
188 name = step.getName()
189
190
191 stripHtml = re.compile(r'<.*?>')
192 strippedDetails = stripHtml.sub('', ' '.join(step.getText()))
193
194 details['buildername'] = builderName
195 details['status'] = strippedDetails
196 details['reason'] = reason
197 logs = details['logs'] = []
198
199 if step.getLogs():
200 for log in step.getLogs():
201 logname = log.getName()
202 logurl = request.childLink(
203 "../builders/%s/builds/%s/steps/%s/logs/%s" %
204 (urllib.quote(builderName),
205 build.getNumber(),
206 urllib.quote(name),
207 urllib.quote(logname)))
208 logs.append(dict(url=logurl, name=logname))
209 return details
210
211 - def getBuildsForRevision(self, request, builder, builderName, lastRevision,
212 numBuilds, debugInfo):
213 """Return the list of all the builds for a given builder that we will
214 need to be able to display the console page. We start by the most recent
215 build, and we go down until we find a build that was built prior to the
216 last change we are interested in."""
217
218 revision = lastRevision
219
220 builds = []
221 build = self.getHeadBuild(builder)
222 number = 0
223 while build and number < numBuilds:
224 debugInfo["builds_scanned"] += 1
225 number += 1
226
227
228
229
230 got_rev = -1
231 try:
232 got_rev = build.getProperty("got_revision")
233 if not self.comparator.isValidRevision(got_rev):
234 got_rev = -1
235 except KeyError:
236 pass
237
238 try:
239 if got_rev == -1:
240 got_rev = build.getProperty("revision")
241 if not self.comparator.isValidRevision(got_rev):
242 got_rev = -1
243 except:
244 pass
245
246
247
248
249
250 if got_rev and got_rev != -1:
251 details = self.getBuildDetails(request, builderName, build)
252 devBuild = DevBuild(got_rev, build, details)
253 builds.append(devBuild)
254
255
256 current_revision = self.getChangeForBuild(
257 build, revision)
258 if self.comparator.isRevisionEarlier(
259 devBuild, current_revision):
260 break
261
262 build = build.getPreviousBuild()
263
264 return builds
265
278
281 """Returns a dictionary of builds we need to inspect to be able to
282 display the console page. The key is the builder name, and the value is
283 an array of build we care about. We also returns a dictionary of
284 builders we care about. The key is it's category.
285
286 lastRevision is the last revision we want to display in the page.
287 categories is a list of categories to display. It is coming from the
288 HTTP GET parameters.
289 builders is a list of builders to display. It is coming from the HTTP
290 GET parameters.
291 """
292
293 allBuilds = dict()
294
295
296 builderList = dict()
297
298 debugInfo["builds_scanned"] = 0
299
300 builderNames = status.getBuilderNames()[:]
301 for builderName in builderNames:
302 builder = status.getBuilder(builderName)
303
304
305 if categories and builder.category not in categories:
306 continue
307 if builders and builderName not in builders:
308 continue
309
310
311 category = builder.category or "default"
312
313
314
315
316
317 category = category.split('|')[0]
318 if not builderList.get(category):
319 builderList[category] = []
320
321
322 builderList[category].append(builderName)
323
324 allBuilds[builderName] = self.getBuildsForRevision(request,
325 builder,
326 builderName,
327 lastRevision,
328 numBuilds,
329 debugInfo)
330
331 return (builderList, allBuilds)
332
333
334
335
336
337
339 """Display the top category line."""
340
341 count = 0
342 for category in builderList:
343 count += len(builderList[category])
344
345 categories = builderList.keys()
346 categories.sort()
347
348 cs = []
349
350 for category in categories:
351 c = {}
352
353
354
355
356 c["name"] = category.lstrip('0123456789')
357
358
359
360
361 c["size"] = (len(builderList[category]) * 100) / count
362 cs.append(c)
363
364 return cs
365
410
412 """Display the boxes that represent the status of each builder in the
413 first build "revision" was in. Returns an HTML list of errors that
414 happened during these builds."""
415
416 details = []
417 nbSlaves = 0
418 for category in builderList:
419 nbSlaves += len(builderList[category])
420
421
422 categories = builderList.keys()
423 categories.sort()
424
425 builds = {}
426
427
428 for category in categories:
429
430 builds[category] = []
431
432
433 for builder in builderList[category]:
434 introducedIn = None
435 firstNotIn = None
436
437
438 for build in allBuilds[builder]:
439 if self.comparator.isRevisionEarlier(build, revision):
440 firstNotIn = build
441 break
442 else:
443 introducedIn = build
444
445
446
447 results = None
448 previousResults = None
449 if introducedIn:
450 results = introducedIn.results
451 if firstNotIn:
452 previousResults = firstNotIn.results
453
454 isRunning = False
455 if introducedIn and not introducedIn.isFinished:
456 isRunning = True
457
458 url = "./waterfall"
459 title = builder
460 tag = ""
461 current_details = {}
462 if introducedIn:
463 current_details = introducedIn.details or ""
464 url = "./buildstatus?builder=%s&number=%s" % (urllib.quote(builder),
465 introducedIn.number)
466 title += " "
467 title += urllib.quote(' '.join(introducedIn.text), ' \n\\/:')
468
469 builderStrip = builder.replace(' ', '')
470 builderStrip = builderStrip.replace('(', '')
471 builderStrip = builderStrip.replace(')', '')
472 builderStrip = builderStrip.replace('.', '')
473 tag = "Tag%s%s" % (builderStrip, introducedIn.number)
474
475 if isRunning:
476 title += ' ETA: %ds' % (introducedIn.eta or 0)
477
478 resultsClass = getResultsClass(results, previousResults, isRunning)
479
480 b = {}
481 b["url"] = url
482 b["title"] = title
483 b["color"] = resultsClass
484 b["tag"] = tag
485
486 builds[category].append(b)
487
488
489
490 if current_details and resultsClass == "failure":
491 details.append(current_details)
492
493 return (builds, details)
494
496 """Filter a set of revisions based on any number of filter criteria.
497 If specified, filter should be a dict with keys corresponding to
498 revision attributes, and values of 1+ strings"""
499 if not filter:
500 if max_revs is None:
501 for rev in reversed(revisions):
502 yield DevRevision(rev)
503 else:
504 for index,rev in enumerate(reversed(revisions)):
505 if index >= max_revs:
506 break
507 yield DevRevision(rev)
508 else:
509 for index, rev in enumerate(reversed(revisions)):
510 if max_revs and index >= max_revs:
511 break
512 try:
513 for field,acceptable in filter.iteritems():
514 if not hasattr(rev, field):
515 raise DoesNotPassFilter
516 if type(acceptable) in (str, unicode):
517 if getattr(rev, field) != acceptable:
518 raise DoesNotPassFilter
519 elif type(acceptable) in (list, tuple, set):
520 if getattr(rev, field) not in acceptable:
521 raise DoesNotPassFilter
522 yield DevRevision(rev)
523 except DoesNotPassFilter:
524 pass
525
526 - def displayPage(self, request, status, builderList, allBuilds, revisions,
527 categories, repository, branch, debugInfo):
528 """Display the console page."""
529
530 subs = dict()
531 subs["branch"] = branch or 'trunk'
532 subs["repository"] = repository
533 if categories:
534 subs["categories"] = ' '.join(categories)
535 subs["time"] = time.strftime("%a %d %b %Y %H:%M:%S",
536 time.localtime(util.now()))
537 subs["debugInfo"] = debugInfo
538 subs["ANYBRANCH"] = ANYBRANCH
539
540 if builderList:
541 subs["categories"] = self.displayCategories(builderList, debugInfo)
542 subs['slaves'] = self.displaySlaveLine(status, builderList, debugInfo)
543 else:
544 subs["categories"] = []
545
546 subs['revisions'] = []
547
548
549 for revision in revisions:
550 r = {}
551
552
553 r['id'] = revision.revision
554 r['link'] = revision.revlink
555 r['who'] = revision.who
556 r['date'] = revision.date
557 r['comments'] = revision.comments
558 r['repository'] = revision.repository
559 r['project'] = revision.project
560
561
562 (builds, details) = self.displayStatusLine(builderList,
563 allBuilds,
564 revision,
565 debugInfo)
566 r['builds'] = builds
567 r['details'] = details
568
569
570 r["span"] = len(builderList) + 2
571
572 subs['revisions'].append(r)
573
574
575
576
577 debugInfo["load_time"] = time.time() - debugInfo["load_time"]
578 return subs
579
580
581 - def content(self, request, cxt):
582 "This method builds the main console view display."
583
584 reload_time = None
585
586
587 if "reload" in request.args:
588 try:
589 reload_time = int(request.args["reload"][0])
590 if reload_time != 0:
591 reload_time = max(reload_time, 15)
592 except ValueError:
593 pass
594
595 request.setHeader('Cache-Control', 'no-cache')
596
597
598 if not reload_time:
599 reload_time = 60
600
601
602 if reload_time is not None and reload_time != 0:
603 cxt['refresh'] = reload_time
604
605
606 debugInfo = cxt['debuginfo'] = dict()
607 debugInfo["load_time"] = time.time()
608
609
610
611 categories = request.args.get("category", [])
612
613 builders = request.args.get("builder", [])
614
615 repository = request.args.get("repository", [None])[0]
616
617 branch = request.args.get("branch", [ANYBRANCH])[0]
618
619 devName = request.args.get("name", [])
620
621
622 status = self.getStatus(request)
623
624
625 source = self.getChangeManager(request)
626 allChanges = self.getAllChanges(source, status, debugInfo)
627
628 debugInfo["source_all"] = len(allChanges)
629
630
631
632
633
634 numRevs = int(request.args.get("revs", [40])[0])
635 if devName:
636 numRevs *= 2
637 numBuilds = numRevs
638
639 revFilter = {}
640 if branch != ANYBRANCH:
641 revFilter['branch'] = branch
642 if devName:
643 revFilter['who'] = devName
644 if repository:
645 revFilter['repository'] = repository
646 revisions = list(self.filterRevisions(allChanges, max_revs=numRevs, filter=revFilter))
647 debugInfo["revision_final"] = len(revisions)
648
649
650
651 builderList = None
652 allBuilds = None
653 if revisions:
654 lastRevision = revisions[len(revisions) - 1].revision
655 debugInfo["last_revision"] = lastRevision
656
657 (builderList, allBuilds) = self.getAllBuildsForRevision(status,
658 request,
659 lastRevision,
660 numBuilds,
661 categories,
662 builders,
663 debugInfo)
664
665 debugInfo["added_blocks"] = 0
666
667 cxt.update(self.displayPage(request, status, builderList, allBuilds,
668 revisions, categories, repository, branch, debugInfo))
669
670 template = request.site.buildbot_service.templates.get_template("console.html")
671 data = template.render(cxt)
672 return data
673
675 """Used for comparing between revisions, as some
676 VCS use a plain counter for revisions (like SVN)
677 while others use different concepts (see Git).
678 """
679
680
681
683 """Used for comparing 2 changes"""
684 raise NotImplementedError
685
687 """Checks whether the revision seems like a VCS revision"""
688 raise NotImplementedError
689
691 raise NotImplementedError
692
695 return first.when < second.when
696
699
701 return operator.attrgetter('when')
702
705 try:
706 return int(first.revision) < int(second.revision)
707 except TypeError:
708 return False
709
711 try:
712 int(revision)
713 return True
714 except:
715 return False
716
718 return operator.attrgetter('revision')
719