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