1   
  2   
  3   
  4   
  5   
  6   
  7   
  8   
  9   
 10   
 11   
 12   
 13   
 14   
 15   
 16   
 17  import re 
 18  from twisted.python import log 
 19  from twisted.spread import pb 
 20  from buildbot.process.buildstep import LoggingBuildStep, RemoteShellCommand 
 21  from buildbot.process.buildstep import RemoteCommand 
 22  from buildbot.status.builder import SUCCESS, WARNINGS, FAILURE, STDOUT, STDERR 
 23  from buildbot.interfaces import BuildSlaveTooOldError 
 24   
 25   
 26   
 27  from buildbot.process.properties import WithProperties 
 28  _hush_pyflakes = [WithProperties] 
 29  del _hush_pyflakes 
 30   
 32      """I run a single shell command on the buildslave. I return FAILURE if 
 33      the exit code of that command is non-zero, SUCCESS otherwise. To change 
 34      this behavior, override my .evaluateCommand method. 
 35   
 36      By default, a failure of this step will mark the whole build as FAILURE. 
 37      To override this, give me an argument of flunkOnFailure=False . 
 38   
 39      I create a single Log named 'log' which contains the output of the 
 40      command. To create additional summary Logs, override my .createSummary 
 41      method. 
 42   
 43      The shell command I run (a list of argv strings) can be provided in 
 44      several ways: 
 45        - a class-level .command attribute 
 46        - a command= parameter to my constructor (overrides .command) 
 47        - set explicitly with my .setCommand() method (overrides both) 
 48   
 49      @ivar command: a list of renderable objects (typically strings or 
 50                     WithProperties instances). This will be used by start() 
 51                     to create a RemoteShellCommand instance. 
 52   
 53      @ivar logfiles: a dict mapping log NAMEs to workdir-relative FILENAMEs 
 54                      of their corresponding logfiles. The contents of the file 
 55                      named FILENAME will be put into a LogFile named NAME, ina 
 56                      something approximating real-time. (note that logfiles= 
 57                      is actually handled by our parent class LoggingBuildStep) 
 58   
 59      @ivar lazylogfiles: Defaults to False. If True, logfiles will be tracked 
 60                          `lazily', meaning they will only be added when and if 
 61                          they are written to. Empty or nonexistent logfiles 
 62                          will be omitted. (Also handled by class 
 63                          LoggingBuildStep.) 
 64   
 65      """ 
 66   
 67      name = "shell" 
 68      description = None  
 69      descriptionDone = None  
 70      command = None  
 71       
 72       
 73       
 74   
 75       
 76       
 77      flunkOnFailure = True 
 78   
 79 -    def __init__(self, workdir=None, 
 80                   description=None, descriptionDone=None, 
 81                   command=None, 
 82                   usePTY="slave-config", 
 83                   **kwargs): 
 118   
121   
123          rkw = self.remote_kwargs 
124          rkw['workdir'] = rkw['workdir'] or workdir 
 125   
128   
130          """Return a list of short strings to describe this step, for the 
131          status display. This uses the first few words of the shell command. 
132          You can replace this by setting .description in your subclass, or by 
133          overriding this method to describe the step better. 
134   
135          @type  done: boolean 
136          @param done: whether the command is complete or not, to improve the 
137                       way the command is described. C{done=False} is used 
138                       while the command is still running, so a single 
139                       imperfect-tense verb is appropriate ('compiling', 
140                       'testing', ...) C{done=True} is used when the command 
141                       has finished, and the default getText() method adds some 
142                       text, so a simple noun is appropriate ('compile', 
143                       'tests' ...) 
144          """ 
145   
146          try: 
147              properties = self.build.getProperties() 
148   
149              if done and self.descriptionDone is not None: 
150                  return properties.render(self.descriptionDone) 
151              if self.description is not None: 
152                  return properties.render(self.description) 
153   
154               
155               
156              if not self.command: 
157                  return ["???"] 
158   
159              words = self.command 
160              if isinstance(words, (str, unicode)): 
161                  words = words.split() 
162               
163              words = properties.render(words) 
164              if len(words) < 1: 
165                  return ["???"] 
166              if len(words) == 1: 
167                  return ["'%s'" % words[0]] 
168              if len(words) == 2: 
169                  return ["'%s" % words[0], "%s'" % words[1]] 
170              return ["'%s" % words[0], "%s" % words[1], "...'"] 
171          except: 
172              log.msg("Error describing step") 
173              log.err() 
174              return ["???"] 
 175   
191               
192               
193   
195          if not self.logfiles: 
196              return  
197          if not self.slaveVersionIsOlderThan("shell", "2.1"): 
198              return  
199           
200           
201           
202           
203          msg1 = ("Warning: buildslave %s is too old " 
204                  "to understand logfiles=, ignoring it." 
205                 % self.getSlaveName()) 
206          msg2 = "You will have to pull this logfile (%s) manually." 
207          log.msg(msg1) 
208          for logname,remotefilevalue in self.logfiles.items(): 
209              remotefilename = remotefilevalue 
210               
211              if type(remotefilevalue) == dict: 
212                  remotefilename = remotefilevalue['filename'] 
213   
214              newlog = self.addLog(logname) 
215              newlog.addHeader(msg1 + "\n") 
216              newlog.addHeader(msg2 % remotefilename + "\n") 
217              newlog.finish() 
218           
219          self.logfiles = {} 
 220   
 245   
246   
247   
249      name = "treesize" 
250      command = ["du", "-s", "-k", "."] 
251      description = "measuring tree size" 
252      descriptionDone = "tree size measured" 
253      kib = None 
254   
257   
264   
271   
272 -    def getText(self, cmd, results): 
 273          if self.kib is not None: 
274              return ["treesize", "%d KiB" % self.kib] 
275          return ["treesize", "unknown"] 
  276   
278      name = "setproperty" 
279   
280 -    def __init__(self, property=None, extract_fn=None, strip=True, **kwargs): 
 295   
311   
313          props_set = [ "%s: %r" % (k,v) for k,v in self.property_changes.items() ] 
314          self.addCompleteLog('property changes', "\n".join(props_set)) 
 315   
316 -    def getText(self, cmd, results): 
 317          if self.property_changes: 
318              return [ "set props:" ] + self.property_changes.keys() 
319          else: 
320              return [ "no change" ] 
  321   
330   
332      """ 
333      FileWriter class that just puts received data into a buffer. 
334   
335      Used to upload a file from slave for inline processing rather than 
336      writing into a file on master. 
337      """ 
340   
343   
 346   
348      """ 
349      Remote command subclass used to run an internal file upload command on the 
350      slave. We do not need any progress updates from such command, so override 
351      remoteUpdate() with an empty method. 
352      """ 
 355   
357      warnCount = 0 
358      warningPattern = '.*warning[: ].*' 
359       
360      directoryEnterPattern = "make.*: Entering directory [\"`'](.*)['`\"]" 
361      directoryLeavePattern = "make.*: Leaving directory" 
362      suppressionFile = None 
363   
364      commentEmptyLineRe = re.compile(r"^\s*(\#.*)?$") 
365      suppressionLineRe = re.compile(r"^\s*(.+?)\s*:\s*(.+?)\s*(?:[:]\s*([0-9]+)(?:-([0-9]+))?\s*)?$") 
366   
367 -    def __init__(self, workdir=None, 
368                   warningPattern=None, warningExtractor=None, 
369                   directoryEnterPattern=None, directoryLeavePattern=None, 
370                   suppressionFile=None, **kwargs): 
 398   
403   
405          """ 
406          This method can be used to add patters of warnings that should 
407          not be counted. 
408   
409          It takes a single argument, a list of patterns. 
410   
411          Each pattern is a 4-tuple (FILE-RE, WARN-RE, START, END). 
412   
413          FILE-RE is a regular expression (string or compiled regexp), or None. 
414          If None, the pattern matches all files, else only files matching the 
415          regexp. If directoryEnterPattern is specified in the class constructor, 
416          matching is against the full path name, eg. src/main.c. 
417   
418          WARN-RE is similarly a regular expression matched against the 
419          text of the warning, or None to match all warnings. 
420   
421          START and END form an inclusive line number range to match against. If 
422          START is None, there is no lower bound, similarly if END is none there 
423          is no upper bound.""" 
424   
425          for fileRe, warnRe, start, end in suppressionList: 
426              if fileRe != None and isinstance(fileRe, str): 
427                  fileRe = re.compile(fileRe) 
428              if warnRe != None and isinstance(warnRe, str): 
429                  warnRe = re.compile(warnRe) 
430              self.suppressions.append((fileRe, warnRe, start, end)) 
 431   
433          """ 
434          Extract warning text as the whole line. 
435          No file names or line numbers.""" 
436          return (None, None, line) 
 437   
439          """ 
440          Extract file name, line number, and warning text as groups (1,2,3) 
441          of warningPattern match.""" 
442          file = match.group(1) 
443          lineNo = match.group(2) 
444          if lineNo != None: 
445              lineNo = int(lineNo) 
446          text = match.group(3) 
447          return (file, lineNo, text) 
 448   
450          if self.suppressions: 
451              (file, lineNo, text) = self.warningExtractor(self, line, match) 
452   
453              if file != None and file != "" and self.directoryStack: 
454                  currentDirectory = self.directoryStack[-1] 
455                  if currentDirectory != None and currentDirectory != "": 
456                      file = "%s/%s" % (currentDirectory, file) 
457   
458               
459              for fileRe, warnRe, start, end in self.suppressions: 
460                  if ( (file == None or fileRe == None or fileRe.search(file)) and 
461                       (warnRe == None or  warnRe.search(text)) and 
462                       ((start == None and end == None) or 
463                        (lineNo != None and start <= lineNo and end >= lineNo)) ): 
464                      return 
465   
466          warnings.append(line) 
467          self.warnCount += 1 
 468   
493   
515   
517          """ 
518          Match log lines against warningPattern. 
519   
520          Warnings are collected into another log for this step, and the 
521          build-wide 'warnings-count' is updated.""" 
522   
523          self.warnCount = 0 
524   
525           
526           
527          if not self.warningPattern: 
528              return 
529   
530          wre = self.warningPattern 
531          if isinstance(wre, str): 
532              wre = re.compile(wre) 
533   
534          directoryEnterRe = self.directoryEnterPattern 
535          if directoryEnterRe != None and isinstance(directoryEnterRe, str): 
536              directoryEnterRe = re.compile(directoryEnterRe) 
537   
538          directoryLeaveRe = self.directoryLeavePattern 
539          if directoryLeaveRe != None and isinstance(directoryLeaveRe, str): 
540              directoryLeaveRe = re.compile(directoryLeaveRe) 
541   
542           
543           
544           
545          warnings = [] 
546           
547           
548          for line in log.getText().split("\n"): 
549              if directoryEnterRe: 
550                  match = directoryEnterRe.search(line) 
551                  if match: 
552                      self.directoryStack.append(match.group(1)) 
553                  if (directoryLeaveRe and 
554                      self.directoryStack and 
555                      directoryLeaveRe.search(line)): 
556                          self.directoryStack.pop() 
557   
558              match = wre.match(line) 
559              if match: 
560                  self.maybeAddWarning(warnings, line, match) 
561   
562           
563           
564          if self.warnCount: 
565              self.addCompleteLog("warnings (%d)" % self.warnCount, 
566                      "\n".join(warnings) + "\n") 
567   
568          warnings_stat = self.step_status.getStatistic('warnings', 0) 
569          self.step_status.setStatistic('warnings', warnings_stat + self.warnCount) 
570   
571          try: 
572              old_count = self.getProperty("warnings-count") 
573          except KeyError: 
574              old_count = 0 
575          self.setProperty("warnings-count", old_count + self.warnCount, "WarningCountingShellCommand") 
 576   
577   
 584   
585   
586 -class Compile(WarningCountingShellCommand): 
 603   
604 -class Test(WarningCountingShellCommand): 
 646   
648      command=["prove", "--lib", "lib", "-r", "t"] 
649      total = 0 
650   
652           
653          lines = map( 
654              lambda line : line.replace('\r\n','').replace('\r','').replace('\n',''), 
655              self.getLog('stdio').readlines() 
656              ) 
657   
658          total = 0 
659          passed = 0 
660          failed = 0 
661          rc = SUCCESS 
662          if cmd.rc > 0: 
663              rc = FAILURE 
664   
665           
666          if "Test Summary Report" in lines: 
667              test_summary_report_index = lines.index("Test Summary Report") 
668              del lines[0:test_summary_report_index + 2] 
669   
670              re_test_result = re.compile("^Result: (PASS|FAIL)$|Tests: \d+ Failed: (\d+)\)|Files=\d+, Tests=(\d+)") 
671   
672              mos = map(lambda line: re_test_result.search(line), lines) 
673              test_result_lines = [mo.groups() for mo in mos if mo] 
674   
675              for line in test_result_lines: 
676                  if line[0] == 'FAIL': 
677                      rc = FAILURE 
678   
679                  if line[1]: 
680                      failed += int(line[1]) 
681                  if line[2]: 
682                      total = int(line[2]) 
683   
684          else:  
685              re_test_result = re.compile("^(All tests successful)|(\d+)/(\d+) subtests failed|Files=\d+, Tests=(\d+),") 
686   
687              mos = map(lambda line: re_test_result.search(line), lines) 
688              test_result_lines = [mo.groups() for mo in mos if mo] 
689   
690              if test_result_lines: 
691                  test_result_line = test_result_lines[0] 
692   
693                  success = test_result_line[0] 
694   
695                  if success: 
696                      failed = 0 
697   
698                      test_totals_line = test_result_lines[1] 
699                      total_str = test_totals_line[3] 
700                  else: 
701                      failed_str = test_result_line[1] 
702                      failed = int(failed_str) 
703   
704                      total_str = test_result_line[2] 
705   
706                      rc = FAILURE 
707   
708                  total = int(total_str) 
709   
710          warnings = 0 
711          if self.warningPattern: 
712              wre = self.warningPattern 
713              if isinstance(wre, str): 
714                  wre = re.compile(wre) 
715   
716              warnings = len([l for l in lines if wre.search(l)]) 
717   
718               
719               
720               
721              if rc == SUCCESS and warnings: 
722                  rc = WARNINGS 
723   
724          if total: 
725              passed = total - failed 
726   
727              self.setTestResults(total=total, failed=failed, passed=passed, 
728                                  warnings=warnings) 
729   
730          return rc 
  731