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
16 """Given the current and past results, return the class that will be used
17 by the css to display the right color for a box."""
18
19 if inProgress:
20 return "running"
21
22 if results is None:
23 return "notstarted"
24
25 if results == builder.SUCCESS:
26 return "success"
27
28 if results == builder.FAILURE:
29 if not prevResults:
30
31
32 return "failure"
33
34 if prevResults != builder.FAILURE:
35
36 return "failure"
37 else:
38
39 return "warnings"
40
41
42 return "exception"
43
45
47 """Helper class that contains all the information we need for a revision."""
48
49 - def __init__(self, revision, who, comments, date, revlink, when):
50 self.revision = revision
51 self.comments = comments
52 self.who = who
53 self.date = date
54 self.revlink = revlink
55 self.when = when
56
57
59 """Helper class that contains all the information we need for a build."""
60
61 - def __init__(self, revision, results, number, isFinished, text, eta, details, when):
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
77 - def __init__(self, allowForce=True, css=None, orderByTime=False):
92
100
103
104 - def head(self, request):
105
106 head = "<script type='text/javascript'> %s </script>" % js.JAVASCRIPT
107
108 reload_time = None
109
110
111 if "reload" in request.args:
112 try:
113 reload_time = int(request.args["reload"][0])
114 if reload_time != 0:
115 reload_time = max(reload_time, 15)
116 except ValueError:
117 pass
118
119
120 if not reload_time:
121 reload_time = 60
122
123
124 if reload_time is not None and reload_time != 0:
125 head += '<meta http-equiv="refresh" content="%d">\n' % reload_time
126 return head
127
128
129
130
131
132
144
145 - def fetchChangesFromHistory(self, status, max_depth, max_builds, debugInfo):
146 """Look at the history of the builders and try to fetch as many changes
147 as possible. We need this when the main source does not contain enough
148 sourcestamps.
149
150 max_depth defines how many builds we will parse for a given builder.
151 max_builds defines how many builds total we want to parse. This is to
152 limit the amount of time we spend in this function.
153
154 This function is sub-optimal, but the information returned by this
155 function is cached, so this function won't be called more than once.
156 """
157
158 allChanges = list()
159 build_count = 0
160 for builderName in status.getBuilderNames()[:]:
161 if build_count > max_builds:
162 break
163
164 builder = status.getBuilder(builderName)
165 build = self.getHeadBuild(builder)
166 depth = 0
167 while build and depth < max_depth and build_count < max_builds:
168 depth += 1
169 build_count += 1
170 sourcestamp = build.getSourceStamp()
171 allChanges.extend(sourcestamp.changes[:])
172 build = build.getPreviousBuild()
173
174 debugInfo["source_fetch_len"] = len(allChanges)
175 return allChanges
176
178 """Return all the changes we can find at this time. If |source| does not
179 not have enough (less than 25), we try to fetch more from the builders
180 history."""
181
182 allChanges = list()
183 allChanges.extend(source.changes[:])
184
185 debugInfo["source_len"] = len(source.changes)
186
187 if len(allChanges) < 25:
188
189
190
191
192
193
194
195 if not self.initialRevs:
196 self.initialRevs = self.fetchChangesFromHistory(status, 10, 100,
197 debugInfo)
198
199 allChanges.extend(self.initialRevs)
200
201
202
203 allChanges.sort(lambda a, b: cmp(getattr(a, self.comparator.getSortingKey()), getattr(b, self.comparator.getSortingKey())))
204
205
206 prevChange = None
207 newChanges = []
208 for change in allChanges:
209 rev = change.revision
210 if not prevChange or rev != prevChange.revision:
211 newChanges.append(change)
212 prevChange = change
213 allChanges = newChanges
214
215 return allChanges
216
218 """Returns a subset of changesn from allChanges that matches the query.
219
220 allChanges is the list of all changes we know about.
221 numRevs is the number of changes we will inspect from allChanges. We
222 do not want to inspect all of them or it would be too slow.
223 branch is the branch we are interested in. Changes not in this branch
224 will be ignored.
225 devName is the developper name. Changes have not been submitted by this
226 person will be ignored.
227 """
228
229 revisions = []
230
231 if not allChanges:
232 return revisions
233
234 totalRevs = len(allChanges)
235 for i in range(totalRevs-1, totalRevs-numRevs, -1):
236 if i < 0:
237 break
238 change = allChanges[i]
239 if branch == ANYBRANCH or branch == change.branch:
240 if not devName or change.who in devName:
241
242 rev = DevRevision(change.revision, change.who,
243 change.comments, change.getTime(),
244 getattr(change, 'revlink', None),
245 change.when)
246 revisions.append(rev)
247
248 return revisions
249
251 """Returns an HTML list of failures for a given build."""
252 details = ""
253 if build.getLogs():
254 for step in build.getSteps():
255 (result, reason) = step.getResults()
256 if result == builder.FAILURE:
257 name = step.getName()
258
259
260 stripHtml = re.compile(r'<.*?>')
261 strippedDetails = stripHtml .sub('', ' '.join(step.getText()))
262
263 details += "<li> %s : %s. \n" % (builderName, strippedDetails)
264 if step.getLogs():
265 details += "[ "
266 for log in step.getLogs():
267 logname = log.getName()
268 logurl = request.childLink(
269 "../builders/%s/builds/%s/steps/%s/logs/%s" %
270 (urllib.quote(builderName),
271 build.getNumber(),
272 urllib.quote(name),
273 urllib.quote(logname)))
274 details += "<a href=\"%s\">%s</a> " % (logurl,
275 log.getName())
276 details += "]"
277 return details
278
279 - def getBuildsForRevision(self, request, builder, builderName, lastRevision,
280 numBuilds, debugInfo):
281 """Return the list of all the builds for a given builder that we will
282 need to be able to display the console page. We start by the most recent
283 build, and we go down until we find a build that was built prior to the
284 last change we are interested in."""
285
286 revision = lastRevision
287
288 builds = []
289 build = self.getHeadBuild(builder)
290 number = 0
291 while build and number < numBuilds:
292 debugInfo["builds_scanned"] += 1
293 number += 1
294
295
296
297
298 got_rev = -1
299 try:
300 got_rev = build.getProperty("got_revision")
301 if not self.comparator.isValidRevision(got_rev):
302 got_rev = -1
303 except KeyError:
304 pass
305
306 try:
307 if got_rev == -1:
308 got_rev = build.getProperty("revision")
309 if not self.comparator.isValidRevision(got_rev):
310 got_rev = -1
311 except:
312 pass
313
314
315
316
317
318 if got_rev and got_rev != -1:
319 details = self.getBuildDetails(request, builderName, build)
320 devBuild = DevBuild(got_rev, build.getResults(),
321 build.getNumber(),
322 build.isFinished(),
323 build.getText(),
324 build.getETA(),
325 details,
326 build.getTimes()[0])
327
328 builds.append(devBuild)
329
330
331 current_revision = self.getChangeForBuild(
332 builder.getBuild(-1), revision)
333 if self.comparator.isRevisionEarlier(
334 devBuild, current_revision):
335 break
336
337 build = build.getPreviousBuild()
338
339 return builds
340
342 if not build.getChanges():
343 devBuild = DevBuild(revision, build.getResults(),
344 build.getNumber(),
345 build.isFinished(),
346 build.getText(),
347 build.getETA(),
348 None,
349 build.getTimes()[0])
350
351 return devBuild
352
353 for change in build.getChanges():
354 if change.revision == revision:
355 return change
356
357
358 changes = list(build.getChanges())
359 changes.sort(lambda a, b: cmp(getattr(a, self.comparator.getSortingKey()), getattr(b, self.comparator.getSortingKey())))
360 return changes[-1]
361
364 """Returns a dictionnary of builds we need to inspect to be able to
365 display the console page. The key is the builder name, and the value is
366 an array of build we care about. We also returns a dictionnary of
367 builders we care about. The key is it's category.
368
369 lastRevision is the last revision we want to display in the page.
370 categories is a list of categories to display. It is coming from the
371 HTTP GET parameters.
372 builders is a list of builders to display. It is coming from the HTTP
373 GET parameters.
374 """
375
376 allBuilds = dict()
377
378
379 builderList = dict()
380
381 debugInfo["builds_scanned"] = 0
382
383 builderNames = status.getBuilderNames()[:]
384 for builderName in builderNames:
385 builder = status.getBuilder(builderName)
386
387
388 if categories and builder.category not in categories:
389 continue
390 if builders and builderName not in builders:
391 continue
392
393
394 category = builder.category or "default"
395
396
397
398
399
400 category = category.split('|')[0]
401 if not builderList.get(category):
402 builderList[category] = []
403
404
405 builderList[category].append(builderName)
406
407 allBuilds[builderName] = self.getBuildsForRevision(request,
408 builder,
409 builderName,
410 lastRevision,
411 numBuilds,
412 debugInfo)
413
414 return (builderList, allBuilds)
415
416
417
418
419
420
422 """Display the top category line."""
423
424 data = res.main_line_category_header.substitute(subs)
425 count = 0
426 for category in builderList:
427 count += len(builderList[category])
428
429 i = 0
430 categories = builderList.keys()
431 categories.sort()
432 for category in categories:
433
434
435
436 subs["first"] = ""
437 subs["last"] = ""
438 if i == 0:
439 subs["first"] = "first"
440 if i == len(builderList) -1:
441 subs["last"] = "last"
442
443
444
445
446
447 subs["category"] = category.lstrip('0123456789')
448
449
450
451
452 subs["size"] = (len(builderList[category]) * 100) / count
453 data += res.main_line_category_name.substitute(subs)
454 i += 1
455 data += res.main_line_category_footer.substitute(subs)
456 return data
457
523
526 """Display the boxes that represent the status of each builder in the
527 first build "revision" was in. Returns an HTML list of errors that
528 happened during these builds."""
529
530 data = ""
531
532
533 subs["last"] = ""
534 if len(builderList) == 1:
535 subs["last"] = "last"
536 data += res.main_line_status_header.substitute(subs)
537
538 details = ""
539 nbSlaves = 0
540 subs["first"] = ""
541 for category in builderList:
542 nbSlaves += len(builderList[category])
543
544 i = 0
545
546 categories = builderList.keys()
547 categories.sort()
548
549
550 for category in categories:
551
552 subs["last"] = ""
553 if i == len(builderList) - 1:
554 subs["last"] = "last"
555
556
557 if i != 0:
558 data += res.main_line_status_section.substitute(subs)
559 i += 1
560
561
562 for builder in builderList[category]:
563 introducedIn = None
564 firstNotIn = None
565
566
567 for build in allBuilds[builder]:
568 if self.comparator.isRevisionEarlier(build, revision):
569 firstNotIn = build
570 break
571 else:
572 introducedIn = build
573
574
575
576 results = None
577 previousResults = None
578 if introducedIn:
579 results = introducedIn.results
580 if firstNotIn:
581 previousResults = firstNotIn.results
582
583 isRunning = False
584 if introducedIn and not introducedIn.isFinished:
585 isRunning = True
586
587 url = "./waterfall"
588 title = builder
589 tag = ""
590 current_details = None
591 if introducedIn:
592 current_details = introducedIn.details or ""
593 url = "./buildstatus?builder=%s&number=%s" % (urllib.quote(builder),
594 introducedIn.number)
595 title += " "
596 title += urllib.quote(' '.join(introducedIn.text), ' \n\\/:')
597
598 builderStrip = builder.replace(' ', '')
599 builderStrip = builderStrip.replace('(', '')
600 builderStrip = builderStrip.replace(')', '')
601 builderStrip = builderStrip.replace('.', '')
602 tag = "Tag%s%s" % (builderStrip, introducedIn.number)
603
604 if isRunning:
605 title += ' ETA: %ds' % (introducedIn.eta or 0)
606
607 resultsClass = getResultsClass(results, previousResults, isRunning)
608 subs["url"] = url
609 subs["title"] = title
610 subs["color"] = resultsClass
611 subs["tag"] = tag
612
613 data += res.main_line_status_box.substitute(subs)
614
615
616
617 if current_details and resultsClass == "failure":
618 details += current_details
619
620 data += res.main_line_status_footer.substitute(subs)
621 return (data, details)
622
623 - def displayPage(self, request, status, builderList, allBuilds, revisions,
624 categories, branch, debugInfo):
625 """Display the console page."""
626
627 subs = dict()
628 subs["projectUrl"] = status.getProjectURL() or ""
629 subs["projectName"] = status.getProjectName() or ""
630 subs["branch"] = branch or 'trunk'
631 if categories:
632 subs["categories"] = ' '.join(categories)
633 subs["welcomeUrl"] = self.path_to_root(request) + "index.html"
634 subs["version"] = version
635 subs["time"] = time.strftime("%a %d %b %Y %H:%M:%S",
636 time.localtime(util.now()))
637 subs["debugInfo"] = debugInfo
638
639
640
641
642
643
644 data = res.top_header.substitute(subs)
645 data += res.top_info_name.substitute(subs)
646
647 if categories:
648 data += res.top_info_categories.substitute(subs)
649
650 if branch != ANYBRANCH:
651 data += res.top_info_branch.substitute(subs)
652
653 data += res.top_info_name_end.substitute(subs)
654
655 data += res.top_legend.substitute(subs)
656
657
658 data += res.top_personalize.substitute(subs)
659
660 data += res.top_footer.substitute(subs)
661
662
663
664
665
666 data += res.main_header.substitute(subs)
667
668
669
670 subs["alt"] = "Alt"
671 subs["first"] = ""
672 subs["last"] = ""
673
674
675 if builderList and len(builderList) > 1:
676 dataToAdd = self.displayCategories(builderList, debugInfo, subs)
677 data += dataToAdd
678
679
680 if builderList:
681 dataToAdd = self.displaySlaveLine(status, builderList, debugInfo,
682 subs)
683 data += dataToAdd
684
685
686 for revision in revisions:
687 if not subs["alt"]:
688 subs["alt"] = "Alt"
689 else:
690 subs["alt"] = ""
691
692
693 subs["revision"] = revision.revision
694 if revision.revlink:
695 subs["revision_link"] = ("<a href=\"%s\">%s</a>"
696 % (revision.revlink,
697 revision.revision))
698 else:
699 subs["revision_link"] = revision.revision
700 subs["who"] = revision.who
701 subs["date"] = revision.date
702 comment = revision.comments or ""
703 subs["comments"] = comment.replace('<', '<').replace('>', '>')
704 comment_quoted = urllib.quote(subs["comments"].encode("utf-8"))
705
706
707 data += res.main_line_info.substitute(subs)
708
709
710 (dataToAdd, details) = self.displayStatusLine(builderList,
711 allBuilds,
712 revision,
713 debugInfo,
714 subs)
715 data += dataToAdd
716
717
718 subs["span"] = len(builderList) + 2
719
720
721 if details:
722 subs["details"] = details
723 data += res.main_line_details.substitute(subs)
724
725
726 data += res.main_line_comments.substitute(subs)
727
728 data += res.main_footer.substitute(subs)
729
730
731
732
733 debugInfo["load_time"] = time.time() - debugInfo["load_time"]
734 data += res.bottom.substitute(subs)
735 return data
736
737 - def body(self, request):
738 "This method builds the main console view display."
739
740
741 debugInfo = dict()
742 debugInfo["load_time"] = time.time()
743
744
745
746 categories = request.args.get("category", [])
747
748 builders = request.args.get("builder", [])
749
750 branch = request.args.get("branch", [ANYBRANCH])[0]
751
752 devName = request.args.get("name", [])
753
754
755 status = self.getStatus(request)
756
757 projectURL = status.getProjectURL()
758 projectName = status.getProjectName()
759
760
761 source = self.getChangemaster(request)
762 allChanges = self.getAllChanges(source, status, debugInfo)
763
764 debugInfo["source_all"] = len(allChanges)
765
766
767
768
769
770 numRevs = 40
771 if devName:
772 numRevs *= 2
773 numBuilds = numRevs
774
775
776 revisions = self.stripRevisions(allChanges, numRevs, branch, devName)
777 debugInfo["revision_final"] = len(revisions)
778
779
780
781 builderList = None
782 allBuilds = None
783 if revisions:
784 lastRevision = revisions[len(revisions)-1].revision
785 debugInfo["last_revision"] = lastRevision
786
787 (builderList, allBuilds) = self.getAllBuildsForRevision(status,
788 request,
789 lastRevision,
790 numBuilds,
791 categories,
792 builders,
793 debugInfo)
794
795 debugInfo["added_blocks"] = 0
796
797 data = ""
798 data += self.displayPage(request, status, builderList, allBuilds,
799 revisions, categories, branch, debugInfo)
800
801 return data
802
804 """Used for comparing between revisions, as some
805 VCS use a plain counter for revisions (like SVN)
806 while others use different concepts (see Git).
807 """
808
809
810
812 """Used for comparing 2 changes"""
813 raise NotImplementedError
814
816 """Checks whether the revision seems like a VCS revision"""
817 raise NotImplementedError
818
820 raise NotImplementedError
821
824 return first.when < second.when
825
828
831
835
837 try:
838 int(revision)
839 return True
840 except:
841 return False
842
845