1   
  2   
  3   
  4   
  5   
  6   
  7   
  8   
  9   
 10   
 11   
 12   
 13   
 14   
 15   
 16   
 17  from twisted.python import log 
 18   
 19  from buildbot.status import testresult 
 20  from buildbot.status.results import SUCCESS, FAILURE, WARNINGS, SKIPPED 
 21  from buildbot.process.buildstep import LogLineObserver, OutputProgressObserver 
 22  from buildbot.steps.shell import ShellCommand 
 23   
 24  try: 
 25      import cStringIO 
 26      StringIO = cStringIO 
 27  except ImportError: 
 28      import StringIO 
 29  import re 
 30   
 31   
 32   
 33 -class HLint(ShellCommand): 
  34      """I run a 'lint' checker over a set of .xhtml files. Any deviations 
 35      from recommended style is flagged and put in the output log. 
 36   
 37      This step looks at .changes in the parent Build to extract a list of 
 38      Lore XHTML files to check.""" 
 39   
 40      name = "hlint" 
 41      description = ["running", "hlint"] 
 42      descriptionDone = ["hlint"] 
 43      warnOnWarnings = True 
 44      warnOnFailure = True 
 45       
 46      warnings = 0 
 47   
 48 -    def __init__(self, python=None, **kwargs): 
  52   
 54           
 55          htmlFiles = {} 
 56          for f in self.build.allFiles(): 
 57              if f.endswith(".xhtml") and not f.startswith("sandbox/"): 
 58                  htmlFiles[f] = 1 
 59           
 60          hlintTargets = htmlFiles.keys() 
 61          hlintTargets.sort() 
 62          if not hlintTargets: 
 63              return SKIPPED 
 64          self.hlintFiles = hlintTargets 
 65          c = [] 
 66          if self.python: 
 67              c.append(self.python) 
 68          c += ["bin/lore", "-p", "--output", "lint"] + self.hlintFiles 
 69          self.setCommand(c) 
 70   
 71           
 72          self.addCompleteLog("files", "\n".join(self.hlintFiles)+"\n") 
 73   
 74          ShellCommand.start(self) 
  75   
 87   
 95   
 96 -    def getText2(self, cmd, results): 
  97          if cmd.rc != 0: 
 98              return ["hlint"] 
 99          return ["%d hlin%s" % (self.warnings, 
100                                 self.warnings == 1 and 't' or 'ts')] 
  101   
103       
104       
105       
106      chunk = output[-10000:] 
107      lines = chunk.split("\n") 
108      lines.pop()  
109       
110       
111       
112       
113       
114       
115      res = {'total': None, 
116             'failures': 0, 
117             'errors': 0, 
118             'skips': 0, 
119             'expectedFailures': 0, 
120             'unexpectedSuccesses': 0, 
121             } 
122      for l in lines: 
123          out = re.search(r'Ran (\d+) tests', l) 
124          if out: 
125              res['total'] = int(out.group(1)) 
126          if (l.startswith("OK") or 
127              l.startswith("FAILED ") or 
128              l.startswith("PASSED")): 
129               
130               
131               
132               
133              out = re.search(r'failures=(\d+)', l) 
134              if out: res['failures'] = int(out.group(1)) 
135              out = re.search(r'errors=(\d+)', l) 
136              if out: res['errors'] = int(out.group(1)) 
137              out = re.search(r'skips=(\d+)', l) 
138              if out: res['skips'] = int(out.group(1)) 
139              out = re.search(r'expectedFailures=(\d+)', l) 
140              if out: res['expectedFailures'] = int(out.group(1)) 
141              out = re.search(r'unexpectedSuccesses=(\d+)', l) 
142              if out: res['unexpectedSuccesses'] = int(out.group(1)) 
143               
144              out = re.search(r'successes=(\d+)', l) 
145              if out: res['successes'] = int(out.group(1)) 
146   
147      return res 
 148   
149   
151      _line_re = re.compile(r'^(?:Doctest: )?([\w\.]+) \.\.\. \[([^\]]+)\]$') 
152      numTests = 0 
153      finished = False 
154   
156           
157           
158           
159           
160           
161           
162           
163           
164   
165          if self.finished: 
166              return 
167          if line.startswith("=" * 40): 
168              self.finished = True 
169              return 
170   
171          m = self._line_re.search(line.strip()) 
172          if m: 
173              testname, result = m.groups() 
174              self.numTests += 1 
175              self.step.setProgress('tests', self.numTests) 
  176   
177   
178  UNSPECIFIED=()  
179   
180 -class Trial(ShellCommand): 
 181      """ 
182      There are some class attributes which may be usefully overridden 
183      by subclasses. 'trialMode' and 'trialArgs' can influence the trial 
184      command line. 
185      """ 
186   
187      name = "trial" 
188      progressMetrics = ('output', 'tests', 'test.log') 
189       
190       
191       
192      logfiles = {"test.log": "_trial_temp/test.log"} 
193       
194   
195      flunkOnFailure = True 
196      python = None 
197      trial = "trial" 
198      trialMode = ["--reporter=bwverbose"]  
199       
200      trialArgs = [] 
201      testpath = UNSPECIFIED  
202      testChanges = False  
203      recurse = False 
204      reactor = None 
205      randomly = False 
206      tests = None  
207   
208 -    def __init__(self, reactor=UNSPECIFIED, python=None, trial=None, 
209                   testpath=UNSPECIFIED, 
210                   tests=None, testChanges=None, 
211                   recurse=None, randomly=None, 
212                   trialMode=None, trialArgs=None, 
213                   **kwargs): 
 214          """ 
215          @type  testpath: string 
216          @param testpath: use in PYTHONPATH when running the tests. If 
217                           None, do not set PYTHONPATH. Setting this to '.' will 
218                           cause the source files to be used in-place. 
219   
220          @type  python: string (without spaces) or list 
221          @param python: which python executable to use. Will form the start of 
222                         the argv array that will launch trial. If you use this, 
223                         you should set 'trial' to an explicit path (like 
224                         /usr/bin/trial or ./bin/trial). Defaults to None, which 
225                         leaves it out entirely (running 'trial args' instead of 
226                         'python ./bin/trial args'). Likely values are 'python', 
227                         ['python2.2'], ['python', '-Wall'], etc. 
228   
229          @type  trial: string 
230          @param trial: which 'trial' executable to run. 
231                        Defaults to 'trial', which will cause $PATH to be 
232                        searched and probably find /usr/bin/trial . If you set 
233                        'python', this should be set to an explicit path (because 
234                        'python2.3 trial' will not work). 
235   
236          @type trialMode: list of strings 
237          @param trialMode: a list of arguments to pass to trial, specifically 
238                            to set the reporting mode. This defaults to ['-to'] 
239                            which means 'verbose colorless output' to the trial 
240                            that comes with Twisted-2.0.x and at least -2.1.0 . 
241                            Newer versions of Twisted may come with a trial 
242                            that prefers ['--reporter=bwverbose']. 
243   
244          @type trialArgs: list of strings 
245          @param trialArgs: a list of arguments to pass to trial, available to 
246                            turn on any extra flags you like. Defaults to []. 
247   
248          @type  tests: list of strings 
249          @param tests: a list of test modules to run, like 
250                        ['twisted.test.test_defer', 'twisted.test.test_process']. 
251                        If this is a string, it will be converted into a one-item 
252                        list. 
253   
254          @type  testChanges: boolean 
255          @param testChanges: if True, ignore the 'tests' parameter and instead 
256                              ask the Build for all the files that make up the 
257                              Changes going into this build. Pass these filenames 
258                              to trial and ask it to look for test-case-name 
259                              tags, running just the tests necessary to cover the 
260                              changes. 
261   
262          @type  recurse: boolean 
263          @param recurse: If True, pass the --recurse option to trial, allowing 
264                          test cases to be found in deeper subdirectories of the 
265                          modules listed in 'tests'. This does not appear to be 
266                          necessary when using testChanges. 
267   
268          @type  reactor: string 
269          @param reactor: which reactor to use, like 'gtk' or 'java'. If not 
270                          provided, the Twisted's usual platform-dependent 
271                          default is used. 
272   
273          @type  randomly: boolean 
274          @param randomly: if True, add the --random=0 argument, which instructs 
275                           trial to run the unit tests in a random order each 
276                           time. This occasionally catches problems that might be 
277                           masked when one module always runs before another 
278                           (like failing to make registerAdapter calls before 
279                           lookups are done). 
280   
281          @type  kwargs: dict 
282          @param kwargs: parameters. The following parameters are inherited from 
283                         L{ShellCommand} and may be useful to set: workdir, 
284                         haltOnFailure, flunkOnWarnings, flunkOnFailure, 
285                         warnOnWarnings, warnOnFailure, want_stdout, want_stderr, 
286                         timeout. 
287          """ 
288          ShellCommand.__init__(self, **kwargs) 
289          self.addFactoryArguments(reactor=reactor, 
290                                   python=python, 
291                                   trial=trial, 
292                                   testpath=testpath, 
293                                   tests=tests, 
294                                   testChanges=testChanges, 
295                                   recurse=recurse, 
296                                   randomly=randomly, 
297                                   trialMode=trialMode, 
298                                   trialArgs=trialArgs, 
299                                   ) 
300   
301          if python: 
302              self.python = python 
303          if self.python is not None: 
304              if type(self.python) is str: 
305                  self.python = [self.python] 
306              for s in self.python: 
307                  if " " in s: 
308                       
309                       
310                       
311                      log.msg("python= component '%s' has spaces") 
312                      log.msg("To add -Wall, use python=['python', '-Wall']") 
313                      why = "python= value has spaces, probably an error" 
314                      raise ValueError(why) 
315   
316          if trial: 
317              self.trial = trial 
318          if " " in self.trial: 
319              raise ValueError("trial= value has spaces") 
320          if trialMode is not None: 
321              self.trialMode = trialMode 
322          if trialArgs is not None: 
323              self.trialArgs = trialArgs 
324   
325          if testpath is not UNSPECIFIED: 
326              self.testpath = testpath 
327          if self.testpath is UNSPECIFIED: 
328              raise ValueError("You must specify testpath= (it can be None)") 
329          assert isinstance(self.testpath, str) or self.testpath is None 
330   
331          if reactor is not UNSPECIFIED: 
332              self.reactor = reactor 
333   
334          if tests is not None: 
335              self.tests = tests 
336          if type(self.tests) is str: 
337              self.tests = [self.tests] 
338          if testChanges is not None: 
339              self.testChanges = testChanges 
340               
341   
342          if not self.testChanges and self.tests is None: 
343              raise ValueError("Must either set testChanges= or provide tests=") 
344   
345          if recurse is not None: 
346              self.recurse = recurse 
347          if randomly is not None: 
348              self.randomly = randomly 
349   
350           
351          command = [] 
352          if self.python: 
353              command.extend(self.python) 
354          command.append(self.trial) 
355          command.extend(self.trialMode) 
356          if self.recurse: 
357              command.append("--recurse") 
358          if self.reactor: 
359              command.append("--reactor=%s" % reactor) 
360          if self.randomly: 
361              command.append("--random=0") 
362          command.extend(self.trialArgs) 
363          self.command = command 
364   
365          if self.reactor: 
366              self.description = ["testing", "(%s)" % self.reactor] 
367              self.descriptionDone = ["tests"] 
368               
369          else: 
370              self.description = ["testing"] 
371              self.descriptionDone = ["tests"] 
372   
373           
374          self.addLogObserver('stdio', TrialTestCaseCounter()) 
375           
376          self.addLogObserver('test.log', OutputProgressObserver('test.log')) 
 377   
379          ShellCommand.setupEnvironment(self, cmd) 
380          if self.testpath != None: 
381              e = cmd.args['env'] 
382              if e is None: 
383                  cmd.args['env'] = {'PYTHONPATH': self.testpath} 
384              else: 
385                   
386                   
387                  if e.get('PYTHONPATH', "") == "": 
388                      e['PYTHONPATH'] = self.testpath 
389                  else: 
390                      e['PYTHONPATH'] = self.testpath + ":" + e['PYTHONPATH'] 
391          try: 
392              p = cmd.args['env']['PYTHONPATH'] 
393              if type(p) is not str: 
394                  log.msg("hey, not a string:", p) 
395                  assert False 
396          except (KeyError, TypeError): 
397               
398               
399               
400              pass 
 401   
414   
415   
417           
418           
419   
420           
421           
422          output = cmd.logs['stdio'].getText() 
423          counts = countFailedTests(output) 
424   
425          total = counts['total'] 
426          failures, errors = counts['failures'], counts['errors'] 
427          parsed = (total != None) 
428          text = [] 
429          text2 = "" 
430   
431          if cmd.rc == 0: 
432              if parsed: 
433                  results = SUCCESS 
434                  if total: 
435                      text += ["%d %s" % \ 
436                               (total, 
437                                total == 1 and "test" or "tests"), 
438                               "passed"] 
439                  else: 
440                      text += ["no tests", "run"] 
441              else: 
442                  results = FAILURE 
443                  text += ["testlog", "unparseable"] 
444                  text2 = "tests" 
445          else: 
446               
447              results = FAILURE 
448              if parsed: 
449                  text.append("tests") 
450                  if failures: 
451                      text.append("%d %s" % \ 
452                                  (failures, 
453                                   failures == 1 and "failure" or "failures")) 
454                  if errors: 
455                      text.append("%d %s" % \ 
456                                  (errors, 
457                                   errors == 1 and "error" or "errors")) 
458                  count = failures + errors 
459                  text2 = "%d tes%s" % (count, (count == 1 and 't' or 'ts')) 
460              else: 
461                  text += ["tests", "failed"] 
462                  text2 = "tests" 
463   
464          if counts['skips']: 
465              text.append("%d %s" %  \ 
466                          (counts['skips'], 
467                           counts['skips'] == 1 and "skip" or "skips")) 
468          if counts['expectedFailures']: 
469              text.append("%d %s" %  \ 
470                          (counts['expectedFailures'], 
471                           counts['expectedFailures'] == 1 and "todo" 
472                           or "todos")) 
473              if 0:  
474                  results = WARNINGS 
475                  if not text2: 
476                      text2 = "todo" 
477   
478          if 0: 
479               
480               
481              if counts['unexpectedSuccesses']: 
482                  text.append("%d surprises" % counts['unexpectedSuccesses']) 
483                  results = WARNINGS 
484                  if not text2: 
485                      text2 = "tests" 
486   
487          if self.reactor: 
488              text.append(self.rtext('(%s)')) 
489              if text2: 
490                  text2 = "%s %s" % (text2, self.rtext('(%s)')) 
491   
492          self.results = results 
493          self.text = text 
494          self.text2 = [text2] 
 495   
496   
497 -    def rtext(self, fmt='%s'): 
 498          if self.reactor: 
499              rtext = fmt % self.reactor 
500              return rtext.replace("reactor", "") 
501          return "" 
 502   
509   
511          output = loog.getText() 
512          problems = "" 
513          sio = StringIO.StringIO(output) 
514          warnings = {} 
515          while 1: 
516              line = sio.readline() 
517              if line == "": 
518                  break 
519              if line.find(" exceptions.DeprecationWarning: ") != -1: 
520                   
521                  warning = line  
522                  warnings[warning] = warnings.get(warning, 0) + 1 
523              elif (line.find(" DeprecationWarning: ") != -1 or 
524                  line.find(" UserWarning: ") != -1): 
525                   
526                  warning = line + sio.readline() 
527                  warnings[warning] = warnings.get(warning, 0) + 1 
528              elif line.find("Warning: ") != -1: 
529                  warning = line 
530                  warnings[warning] = warnings.get(warning, 0) + 1 
531   
532              if line.find("=" * 60) == 0 or line.find("-" * 60) == 0: 
533                  problems += line 
534                  problems += sio.read() 
535                  break 
536   
537          if problems: 
538              self.addCompleteLog("problems", problems) 
539               
540              pio = StringIO.StringIO(problems) 
541              pio.readline()  
542              testname = None 
543              done = False 
544              while not done: 
545                  while 1: 
546                      line = pio.readline() 
547                      if line == "": 
548                          done = True 
549                          break 
550                      if line.find("=" * 60) == 0: 
551                          break 
552                      if line.find("-" * 60) == 0: 
553                           
554                           
555                          done = True 
556                          break 
557                      if testname is None: 
558                           
559   
560   
561   
562                          r = re.search(r'^([^:]+): (\w+) \(([\w\.]+)\)', line) 
563                          if not r: 
564                               
565                               
566                              continue 
567                          result, name, case = r.groups() 
568                          testname = tuple(case.split(".") + [name]) 
569                          results = {'SKIPPED': SKIPPED, 
570                                     'EXPECTED FAILURE': SUCCESS, 
571                                     'UNEXPECTED SUCCESS': WARNINGS, 
572                                     'FAILURE': FAILURE, 
573                                     'ERROR': FAILURE, 
574                                     'SUCCESS': SUCCESS,  
575                                     }.get(result, WARNINGS) 
576                          text = result.lower().split() 
577                          loog = line 
578                           
579                          loog += pio.readline() 
580                      else: 
581                           
582                          loog += line 
583                  if testname: 
584                      self.addTestResult(testname, results, text, loog) 
585                      testname = None 
586   
587          if warnings: 
588              lines = warnings.keys() 
589              lines.sort() 
590              self.addCompleteLog("warnings", "".join(lines)) 
 591   
594   
595 -    def getText(self, cmd, results): 
 597 -    def getText2(self, cmd, results): 
  599   
600   
602      name = "remove-.pyc" 
603      command = ['find', '.', '-name', '*.pyc', '-exec', 'rm', '{}', ';'] 
604      description = ["removing", ".pyc", "files"] 
605      descriptionDone = ["remove", ".pycs"] 
 606