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