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): 
  51   
 53           
 54          htmlFiles = {} 
 55          for f in self.build.allFiles(): 
 56              if f.endswith(".xhtml") and not f.startswith("sandbox/"): 
 57                  htmlFiles[f] = 1 
 58           
 59          hlintTargets = htmlFiles.keys() 
 60          hlintTargets.sort() 
 61          if not hlintTargets: 
 62              return SKIPPED 
 63          self.hlintFiles = hlintTargets 
 64          c = [] 
 65          if self.python: 
 66              c.append(self.python) 
 67          c += ["bin/lore", "-p", "--output", "lint"] + self.hlintFiles 
 68          self.setCommand(c) 
 69   
 70           
 71          self.addCompleteLog("files", "\n".join(self.hlintFiles)+"\n") 
 72   
 73          ShellCommand.start(self) 
  74   
 86   
 94   
 95 -    def getText2(self, cmd, results): 
  96          if cmd.didFail(): 
 97              return ["hlint"] 
 98          return ["%d hlin%s" % (self.warnings, 
 99                                 self.warnings == 1 and 't' or 'ts')] 
  100   
102       
103       
104       
105      chunk = output[-10000:] 
106      lines = chunk.split("\n") 
107      lines.pop()  
108       
109       
110       
111       
112       
113       
114      res = {'total': None, 
115             'failures': 0, 
116             'errors': 0, 
117             'skips': 0, 
118             'expectedFailures': 0, 
119             'unexpectedSuccesses': 0, 
120             } 
121      for l in lines: 
122          out = re.search(r'Ran (\d+) tests', l) 
123          if out: 
124              res['total'] = int(out.group(1)) 
125          if (l.startswith("OK") or 
126              l.startswith("FAILED ") or 
127              l.startswith("PASSED")): 
128               
129               
130               
131               
132              out = re.search(r'failures=(\d+)', l) 
133              if out: res['failures'] = int(out.group(1)) 
134              out = re.search(r'errors=(\d+)', l) 
135              if out: res['errors'] = int(out.group(1)) 
136              out = re.search(r'skips=(\d+)', l) 
137              if out: res['skips'] = int(out.group(1)) 
138              out = re.search(r'expectedFailures=(\d+)', l) 
139              if out: res['expectedFailures'] = int(out.group(1)) 
140              out = re.search(r'unexpectedSuccesses=(\d+)', l) 
141              if out: res['unexpectedSuccesses'] = int(out.group(1)) 
142               
143              out = re.search(r'successes=(\d+)', l) 
144              if out: res['successes'] = int(out.group(1)) 
145   
146      return res 
 147   
148   
150      _line_re = re.compile(r'^(?:Doctest: )?([\w\.]+) \.\.\. \[([^\]]+)\]$') 
151      numTests = 0 
152      finished = False 
153   
155           
156           
157           
158           
159           
160           
161           
162           
163   
164          if self.finished: 
165              return 
166          if line.startswith("=" * 40): 
167              self.finished = True 
168              return 
169   
170          m = self._line_re.search(line.strip()) 
171          if m: 
172              testname, result = m.groups() 
173              self.numTests += 1 
174              self.step.setProgress('tests', self.numTests) 
  175   
176   
177  UNSPECIFIED=()  
178   
179 -class Trial(ShellCommand): 
 180      """ 
181      There are some class attributes which may be usefully overridden 
182      by subclasses. 'trialMode' and 'trialArgs' can influence the trial 
183      command line. 
184      """ 
185   
186      name = "trial" 
187      progressMetrics = ('output', 'tests', 'test.log') 
188       
189       
190       
191      logfiles = {"test.log": "_trial_temp/test.log"} 
192       
193   
194      renderables = ['tests'] 
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   
290          if python: 
291              self.python = python 
292          if self.python is not None: 
293              if type(self.python) is str: 
294                  self.python = [self.python] 
295              for s in self.python: 
296                  if " " in s: 
297                       
298                       
299                       
300                      log.msg("python= component '%s' has spaces") 
301                      log.msg("To add -Wall, use python=['python', '-Wall']") 
302                      why = "python= value has spaces, probably an error" 
303                      raise ValueError(why) 
304   
305          if trial: 
306              self.trial = trial 
307          if " " in self.trial: 
308              raise ValueError("trial= value has spaces") 
309          if trialMode is not None: 
310              self.trialMode = trialMode 
311          if trialArgs is not None: 
312              self.trialArgs = trialArgs 
313   
314          if testpath is not UNSPECIFIED: 
315              self.testpath = testpath 
316          if self.testpath is UNSPECIFIED: 
317              raise ValueError("You must specify testpath= (it can be None)") 
318          assert isinstance(self.testpath, str) or self.testpath is None 
319   
320          if reactor is not UNSPECIFIED: 
321              self.reactor = reactor 
322   
323          if tests is not None: 
324              self.tests = tests 
325          if type(self.tests) is str: 
326              self.tests = [self.tests] 
327          if testChanges is not None: 
328              self.testChanges = testChanges 
329               
330   
331          if not self.testChanges and self.tests is None: 
332              raise ValueError("Must either set testChanges= or provide tests=") 
333   
334          if recurse is not None: 
335              self.recurse = recurse 
336          if randomly is not None: 
337              self.randomly = randomly 
338   
339           
340          command = [] 
341          if self.python: 
342              command.extend(self.python) 
343          command.append(self.trial) 
344          command.extend(self.trialMode) 
345          if self.recurse: 
346              command.append("--recurse") 
347          if self.reactor: 
348              command.append("--reactor=%s" % reactor) 
349          if self.randomly: 
350              command.append("--random=0") 
351          command.extend(self.trialArgs) 
352          self.command = command 
353   
354          if self.reactor: 
355              self.description = ["testing", "(%s)" % self.reactor] 
356              self.descriptionDone = ["tests"] 
357               
358          else: 
359              self.description = ["testing"] 
360              self.descriptionDone = ["tests"] 
361   
362           
363          self.addLogObserver('stdio', TrialTestCaseCounter()) 
364           
365          self.addLogObserver('test.log', OutputProgressObserver('test.log')) 
 366   
382   
395   
396   
398           
399           
400   
401           
402           
403          output = cmd.logs['stdio'].getText() 
404          counts = countFailedTests(output) 
405   
406          total = counts['total'] 
407          failures, errors = counts['failures'], counts['errors'] 
408          parsed = (total != None) 
409          text = [] 
410          text2 = "" 
411   
412          if not cmd.didFail(): 
413              if parsed: 
414                  results = SUCCESS 
415                  if total: 
416                      text += ["%d %s" % \ 
417                               (total, 
418                                total == 1 and "test" or "tests"), 
419                               "passed"] 
420                  else: 
421                      text += ["no tests", "run"] 
422              else: 
423                  results = FAILURE 
424                  text += ["testlog", "unparseable"] 
425                  text2 = "tests" 
426          else: 
427               
428              results = FAILURE 
429              if parsed: 
430                  text.append("tests") 
431                  if failures: 
432                      text.append("%d %s" % \ 
433                                  (failures, 
434                                   failures == 1 and "failure" or "failures")) 
435                  if errors: 
436                      text.append("%d %s" % \ 
437                                  (errors, 
438                                   errors == 1 and "error" or "errors")) 
439                  count = failures + errors 
440                  text2 = "%d tes%s" % (count, (count == 1 and 't' or 'ts')) 
441              else: 
442                  text += ["tests", "failed"] 
443                  text2 = "tests" 
444   
445          if counts['skips']: 
446              text.append("%d %s" %  \ 
447                          (counts['skips'], 
448                           counts['skips'] == 1 and "skip" or "skips")) 
449          if counts['expectedFailures']: 
450              text.append("%d %s" %  \ 
451                          (counts['expectedFailures'], 
452                           counts['expectedFailures'] == 1 and "todo" 
453                           or "todos")) 
454              if 0:  
455                  results = WARNINGS 
456                  if not text2: 
457                      text2 = "todo" 
458   
459          if 0: 
460               
461               
462              if counts['unexpectedSuccesses']: 
463                  text.append("%d surprises" % counts['unexpectedSuccesses']) 
464                  results = WARNINGS 
465                  if not text2: 
466                      text2 = "tests" 
467   
468          if self.reactor: 
469              text.append(self.rtext('(%s)')) 
470              if text2: 
471                  text2 = "%s %s" % (text2, self.rtext('(%s)')) 
472   
473          self.results = results 
474          self.text = text 
475          self.text2 = [text2] 
 476   
477   
478 -    def rtext(self, fmt='%s'): 
 479          if self.reactor: 
480              rtext = fmt % self.reactor 
481              return rtext.replace("reactor", "") 
482          return "" 
 483   
490   
492          output = loog.getText() 
493          problems = "" 
494          sio = StringIO.StringIO(output) 
495          warnings = {} 
496          while 1: 
497              line = sio.readline() 
498              if line == "": 
499                  break 
500              if line.find(" exceptions.DeprecationWarning: ") != -1: 
501                   
502                  warning = line  
503                  warnings[warning] = warnings.get(warning, 0) + 1 
504              elif (line.find(" DeprecationWarning: ") != -1 or 
505                  line.find(" UserWarning: ") != -1): 
506                   
507                  warning = line + sio.readline() 
508                  warnings[warning] = warnings.get(warning, 0) + 1 
509              elif line.find("Warning: ") != -1: 
510                  warning = line 
511                  warnings[warning] = warnings.get(warning, 0) + 1 
512   
513              if line.find("=" * 60) == 0 or line.find("-" * 60) == 0: 
514                  problems += line 
515                  problems += sio.read() 
516                  break 
517   
518          if problems: 
519              self.addCompleteLog("problems", problems) 
520               
521              pio = StringIO.StringIO(problems) 
522              pio.readline()  
523              testname = None 
524              done = False 
525              while not done: 
526                  while 1: 
527                      line = pio.readline() 
528                      if line == "": 
529                          done = True 
530                          break 
531                      if line.find("=" * 60) == 0: 
532                          break 
533                      if line.find("-" * 60) == 0: 
534                           
535                           
536                          done = True 
537                          break 
538                      if testname is None: 
539                           
540   
541   
542   
543                          r = re.search(r'^([^:]+): (\w+) \(([\w\.]+)\)', line) 
544                          if not r: 
545                               
546                               
547                              continue 
548                          result, name, case = r.groups() 
549                          testname = tuple(case.split(".") + [name]) 
550                          results = {'SKIPPED': SKIPPED, 
551                                     'EXPECTED FAILURE': SUCCESS, 
552                                     'UNEXPECTED SUCCESS': WARNINGS, 
553                                     'FAILURE': FAILURE, 
554                                     'ERROR': FAILURE, 
555                                     'SUCCESS': SUCCESS,  
556                                     }.get(result, WARNINGS) 
557                          text = result.lower().split() 
558                          loog = line 
559                           
560                          loog += pio.readline() 
561                      else: 
562                           
563                          loog += line 
564                  if testname: 
565                      self.addTestResult(testname, results, text, loog) 
566                      testname = None 
567   
568          if warnings: 
569              lines = warnings.keys() 
570              lines.sort() 
571              self.addCompleteLog("warnings", "".join(lines)) 
 572   
575   
576 -    def getText(self, cmd, results): 
 578 -    def getText2(self, cmd, results): 
  580   
581   
583      name = "remove-.pyc" 
584      command = ['find', '.', '-name', '*.pyc', '-exec', 'rm', '{}', ';'] 
585      description = ["removing", ".pyc", "files"] 
586      descriptionDone = ["remove", ".pycs"] 
 587