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