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 """Given the current and past results, return the class that will be used
14 by the css to display the right color for a box."""
15
16 if inProgress:
17 return "running"
18
19 if results is None:
20 return "notstarted"
21
22 if results == builder.SUCCESS:
23 return "success"
24
25 if results == builder.FAILURE:
26 if not prevResults:
27
28
29 return "failure"
30
31 if prevResults != builder.FAILURE:
32
33 return "failure"
34 else:
35
36 return "warnings"
37
38
39 return "exception"
40
42
44 """Helper class that contains all the information we need for a revision."""
45
55
56
58 """Helper class that contains all the information we need for a build."""
59
60 - def __init__(self, revision, build, details):
70
71
73 """Main console class. It displays a user-oriented status page.
74 Every change is a line in the page, and it shows the result of the first
75 build with this change for each slave."""
76
86
94
97
98
99
100
101
113
114 - def fetchChangesFromHistory(self, status, max_depth, max_builds, debugInfo):
115 """Look at the history of the builders and try to fetch as many changes
116 as possible. We need this when the main source does not contain enough
117 sourcestamps.
118
119 max_depth defines how many builds we will parse for a given builder.
120 max_builds defines how many builds total we want to parse. This is to
121 limit the amount of time we spend in this function.
122
123 This function is sub-optimal, but the information returned by this
124 function is cached, so this function won't be called more than once.
125 """
126
127 allChanges = list()
128 build_count = 0
129 for builderName in status.getBuilderNames()[:]:
130 if build_count > max_builds:
131 break
132
133 builder = status.getBuilder(builderName)
134 build = self.getHeadBuild(builder)
135 depth = 0
136 while build and depth < max_depth and build_count < max_builds:
137 depth += 1
138 build_count += 1
139 sourcestamp = build.getSourceStamp()
140 allChanges.extend(sourcestamp.changes[:])
141 build = build.getPreviousBuild()
142
143 debugInfo["source_fetch_len"] = len(allChanges)
144 return allChanges
145
147 """Return all the changes we can find at this time. If |source| does not
148 not have enough (less than 25), we try to fetch more from the builders
149 history."""
150
151 g = source.eventGenerator()
152 allChanges = []
153 while len(allChanges) < 25:
154 try:
155 c = g.next()
156 except StopIteration:
157 break
158 allChanges.append(c)
159
160 allChanges.sort(key=self.comparator.getSortingKey())
161
162
163 prevChange = None
164 newChanges = []
165 for change in allChanges:
166 rev = change.revision
167 if not prevChange or rev != prevChange.revision:
168 newChanges.append(change)
169 prevChange = change
170 allChanges = newChanges
171
172 return allChanges
173
175 """Returns a subset of changes from allChanges that matches the query.
176
177 allChanges is the list of all changes we know about.
178 numRevs is the number of changes we will inspect from allChanges. We
179 do not want to inspect all of them or it would be too slow.
180 branch is the branch we are interested in. Changes not in this branch
181 will be ignored.
182 devName is the developper name. Changes have not been submitted by this
183 person will be ignored.
184 """
185
186 revisions = []
187
188 if not allChanges:
189 return revisions
190
191 totalRevs = len(allChanges)
192 for i in range(totalRevs - 1, totalRevs - numRevs, -1):
193 if i < 0:
194 break
195 change = allChanges[i]
196 if branch == ANYBRANCH or branch == change.branch:
197 if not devName or change.who in devName:
198 rev = DevRevision(change)
199 revisions.append(rev)
200
201 return revisions
202
204 """Returns an HTML list of failures for a given build."""
205 details = {}
206 if not build.getLogs():
207 return details
208
209 for step in build.getSteps():
210 (result, reason) = step.getResults()
211 if result == builder.FAILURE:
212 name = step.getName()
213
214
215 stripHtml = re.compile(r'<.*?>')
216 strippedDetails = stripHtml.sub('', ' '.join(step.getText()))
217
218 details['buildername'] = builderName
219 details['status'] = strippedDetails
220 details['reason'] = reason
221 logs = details['logs'] = []
222
223 if step.getLogs():
224 for log in step.getLogs():
225 logname = log.getName()
226 logurl = request.childLink(
227 "../builders/%s/builds/%s/steps/%s/logs/%s" %
228 (urllib.quote(builderName),
229 build.getNumber(),
230 urllib.quote(name),
231 urllib.quote(logname)))
232 logs.append(dict(url=logurl, name=logname))
233 return details
234
235 - def getBuildsForRevision(self, request, builder, builderName, lastRevision,
236 numBuilds, debugInfo):
237 """Return the list of all the builds for a given builder that we will
238 need to be able to display the console page. We start by the most recent
239 build, and we go down until we find a build that was built prior to the
240 last change we are interested in."""
241
242 revision = lastRevision
243
244 builds = []
245 build = self.getHeadBuild(builder)
246 number = 0
247 while build and number < numBuilds:
248 debugInfo["builds_scanned"] += 1
249 number += 1
250
251
252
253
254 got_rev = -1
255 try:
256 got_rev = build.getProperty("got_revision")
257 if not self.comparator.isValidRevision(got_rev):
258 got_rev = -1
259 except KeyError:
260 pass
261
262 try:
263 if got_rev == -1:
264 got_rev = build.getProperty("revision")
265 if not self.comparator.isValidRevision(got_rev):
266 got_rev = -1
267 except:
268 pass
269
270
271
272
273
274 if got_rev and got_rev != -1:
275 details = self.getBuildDetails(request, builderName, build)
276 devBuild = DevBuild(got_rev, build, details)
277 builds.append(devBuild)
278
279
280 current_revision = self.getChangeForBuild(
281 build, revision)
282 if self.comparator.isRevisionEarlier(
283 devBuild, current_revision):
284 break
285
286 build = build.getPreviousBuild()
287
288 return builds
289
302
305 """Returns a dictionnary of builds we need to inspect to be able to
306 display the console page. The key is the builder name, and the value is
307 an array of build we care about. We also returns a dictionnary of
308 builders we care about. The key is it's category.
309
310 lastRevision is the last revision we want to display in the page.
311 categories is a list of categories to display. It is coming from the
312 HTTP GET parameters.
313 builders is a list of builders to display. It is coming from the HTTP
314 GET parameters.
315 """
316
317 allBuilds = dict()
318
319
320 builderList = dict()
321
322 debugInfo["builds_scanned"] = 0
323
324 builderNames = status.getBuilderNames()[:]
325 for builderName in builderNames:
326 builder = status.getBuilder(builderName)
327
328
329 if categories and builder.category not in categories:
330 continue
331 if builders and builderName not in builders:
332 continue
333
334
335 category = builder.category or "default"
336
337
338
339
340
341 category = category.split('|')[0]
342 if not builderList.get(category):
343 builderList[category] = []
344
345
346 builderList[category].append(builderName)
347
348 allBuilds[builderName] = self.getBuildsForRevision(request,
349 builder,
350 builderName,
351 lastRevision,
352 numBuilds,
353 debugInfo)
354
355 return (builderList, allBuilds)
356
357
358
359
360
361
363 """Display the top category line."""
364
365 count = 0
366 for category in builderList:
367 count += len(builderList[category])
368
369 categories = builderList.keys()
370 categories.sort()
371
372 cs = []
373
374 for category in categories:
375 c = {}
376
377
378
379
380 c["name"] = category.lstrip('0123456789')
381
382
383
384
385 c["size"] = (len(builderList[category]) * 100) / count
386 cs.append(c)
387
388 return cs
389
434
436 """Display the boxes that represent the status of each builder in the
437 first build "revision" was in. Returns an HTML list of errors that
438 happened during these builds."""
439
440 details = []
441 nbSlaves = 0
442 for category in builderList:
443 nbSlaves += len(builderList[category])
444
445
446 categories = builderList.keys()
447 categories.sort()
448
449 builds = {}
450
451
452 for category in categories:
453
454 builds[category] = []
455
456
457 for builder in builderList[category]:
458 introducedIn = None
459 firstNotIn = None
460
461
462 for build in allBuilds[builder]:
463 if self.comparator.isRevisionEarlier(build, revision):
464 firstNotIn = build
465 break
466 else:
467 introducedIn = build
468
469
470
471 results = None
472 previousResults = None
473 if introducedIn:
474 results = introducedIn.results
475 if firstNotIn:
476 previousResults = firstNotIn.results
477
478 isRunning = False
479 if introducedIn and not introducedIn.isFinished:
480 isRunning = True
481
482 url = "./waterfall"
483 title = builder
484 tag = ""
485 current_details = {}
486 if introducedIn:
487 current_details = introducedIn.details or ""
488 url = "./buildstatus?builder=%s&number=%s" % (urllib.quote(builder),
489 introducedIn.number)
490 title += " "
491 title += urllib.quote(' '.join(introducedIn.text), ' \n\\/:')
492
493 builderStrip = builder.replace(' ', '')
494 builderStrip = builderStrip.replace('(', '')
495 builderStrip = builderStrip.replace(')', '')
496 builderStrip = builderStrip.replace('.', '')
497 tag = "Tag%s%s" % (builderStrip, introducedIn.number)
498
499 if isRunning:
500 title += ' ETA: %ds' % (introducedIn.eta or 0)
501
502 resultsClass = getResultsClass(results, previousResults, isRunning)
503
504 b = {}
505 b["url"] = url
506 b["title"] = title
507 b["color"] = resultsClass
508 b["tag"] = tag
509
510 builds[category].append(b)
511
512
513
514 if current_details and resultsClass == "failure":
515 details.append(current_details)
516
517 return (builds, details)
518
519 - def displayPage(self, request, status, builderList, allBuilds, revisions,
520 categories, branch, debugInfo):
521 """Display the console page."""
522
523 subs = dict()
524 subs["branch"] = branch or 'trunk'
525 if categories:
526 subs["categories"] = ' '.join(categories)
527 subs["time"] = time.strftime("%a %d %b %Y %H:%M:%S",
528 time.localtime(util.now()))
529 subs["debugInfo"] = debugInfo
530 subs["ANYBRANCH"] = ANYBRANCH
531
532 if builderList:
533 subs["categories"] = self.displayCategories(builderList, debugInfo)
534 subs['slaves'] = self.displaySlaveLine(status, builderList, debugInfo)
535 else:
536 subs["categories"] = []
537
538 subs['revisions'] = []
539
540
541 for revision in revisions:
542 r = {}
543
544
545 r['id'] = revision.revision
546 r['link'] = revision.revlink
547 r['who'] = revision.who
548 r['date'] = revision.date
549 r['comments'] = revision.comments
550 r['repository'] = revision.repository
551 r['project'] = revision.project
552
553
554 (builds, details) = self.displayStatusLine(builderList,
555 allBuilds,
556 revision,
557 debugInfo)
558 r['builds'] = builds
559 r['details'] = details
560
561
562 r["span"] = len(builderList) + 2
563
564 subs['revisions'].append(r)
565
566
567
568
569 debugInfo["load_time"] = time.time() - debugInfo["load_time"]
570 return subs
571
572
573 - def content(self, request, cxt):
574 "This method builds the main console view display."
575
576 reload_time = None
577
578
579 if "reload" in request.args:
580 try:
581 reload_time = int(request.args["reload"][0])
582 if reload_time != 0:
583 reload_time = max(reload_time, 15)
584 except ValueError:
585 pass
586
587
588 if not reload_time:
589 reload_time = 60
590
591
592 if reload_time is not None and reload_time != 0:
593 cxt['refresh'] = reload_time
594
595
596 debugInfo = cxt['debuginfo'] = dict()
597 debugInfo["load_time"] = time.time()
598
599
600
601 categories = request.args.get("category", [])
602
603 builders = request.args.get("builder", [])
604
605 branch = request.args.get("branch", [ANYBRANCH])[0]
606
607 devName = request.args.get("name", [])
608
609
610 status = self.getStatus(request)
611
612
613 source = self.getChangeManager(request)
614 allChanges = self.getAllChanges(source, status, debugInfo)
615
616 debugInfo["source_all"] = len(allChanges)
617
618
619
620
621
622 numRevs = 40
623 if devName:
624 numRevs *= 2
625 numBuilds = numRevs
626
627
628 revisions = self.stripRevisions(allChanges, numRevs, branch, devName)
629 debugInfo["revision_final"] = len(revisions)
630
631
632
633 builderList = None
634 allBuilds = None
635 if revisions:
636 lastRevision = revisions[len(revisions) - 1].revision
637 debugInfo["last_revision"] = lastRevision
638
639 (builderList, allBuilds) = self.getAllBuildsForRevision(status,
640 request,
641 lastRevision,
642 numBuilds,
643 categories,
644 builders,
645 debugInfo)
646
647 debugInfo["added_blocks"] = 0
648
649 cxt.update(self.displayPage(request, status, builderList, allBuilds,
650 revisions, categories, branch, debugInfo))
651
652 template = request.site.buildbot_service.templates.get_template("console.html")
653 data = template.render(cxt)
654 return data
655
657 """Used for comparing between revisions, as some
658 VCS use a plain counter for revisions (like SVN)
659 while others use different concepts (see Git).
660 """
661
662
663
665 """Used for comparing 2 changes"""
666 raise NotImplementedError
667
669 """Checks whether the revision seems like a VCS revision"""
670 raise NotImplementedError
671
673 raise NotImplementedError
674
677 return first.when < second.when
678
681
683 return operator.attrgetter('when')
684
688
690 try:
691 int(revision)
692 return True
693 except:
694 return False
695
697 return operator.attrgetter('revision')
698