Always clean the source tree before doing a fetch.
[buildbot.git] / buildbot / steps / python_twisted.py
blobd0ed5b08845e28a8452ee6d92177b20453767f49
1 # -*- test-case-name: buildbot.test.test_twisted -*-
3 from twisted.python import log
5 from buildbot.status import builder
6 from buildbot.status.builder import SUCCESS, FAILURE, WARNINGS, SKIPPED
7 from buildbot.process.buildstep import LogLineObserver, OutputProgressObserver
8 from buildbot.process.buildstep import RemoteShellCommand
9 from buildbot.steps.shell import ShellCommand
11 try:
12 import cStringIO
13 StringIO = cStringIO
14 except ImportError:
15 import StringIO
16 import re
18 # BuildSteps that are specific to the Twisted source tree
20 class HLint(ShellCommand):
21 """I run a 'lint' checker over a set of .xhtml files. Any deviations
22 from recommended style is flagged and put in the output log.
24 This step looks at .changes in the parent Build to extract a list of
25 Lore XHTML files to check."""
27 name = "hlint"
28 description = ["running", "hlint"]
29 descriptionDone = ["hlint"]
30 warnOnWarnings = True
31 warnOnFailure = True
32 # TODO: track time, but not output
33 warnings = 0
35 def __init__(self, python=None, **kwargs):
36 ShellCommand.__init__(self, **kwargs)
37 self.addFactoryArguments(python=python)
38 self.python = python
40 def start(self):
41 # create the command
42 htmlFiles = {}
43 for f in self.build.allFiles():
44 if f.endswith(".xhtml") and not f.startswith("sandbox/"):
45 htmlFiles[f] = 1
46 # remove duplicates
47 hlintTargets = htmlFiles.keys()
48 hlintTargets.sort()
49 if not hlintTargets:
50 return SKIPPED
51 self.hlintFiles = hlintTargets
52 c = []
53 if self.python:
54 c.append(self.python)
55 c += ["bin/lore", "-p", "--output", "lint"] + self.hlintFiles
56 self.setCommand(c)
58 # add an extra log file to show the .html files we're checking
59 self.addCompleteLog("files", "\n".join(self.hlintFiles)+"\n")
61 ShellCommand.start(self)
63 def commandComplete(self, cmd):
64 # TODO: remove the 'files' file (a list of .xhtml files that were
65 # submitted to hlint) because it is available in the logfile and
66 # mostly exists to give the user an idea of how long the step will
67 # take anyway).
68 lines = cmd.logs['stdio'].getText().split("\n")
69 warningLines = filter(lambda line:':' in line, lines)
70 if warningLines:
71 self.addCompleteLog("warnings", "".join(warningLines))
72 warnings = len(warningLines)
73 self.warnings = warnings
75 def evaluateCommand(self, cmd):
76 # warnings are in stdout, rc is always 0, unless the tools break
77 if cmd.rc != 0:
78 return FAILURE
79 if self.warnings:
80 return WARNINGS
81 return SUCCESS
83 def getText2(self, cmd, results):
84 if cmd.rc != 0:
85 return ["hlint"]
86 return ["%d hlin%s" % (self.warnings,
87 self.warnings == 1 and 't' or 'ts')]
89 def countFailedTests(output):
90 # start scanning 10kb from the end, because there might be a few kb of
91 # import exception tracebacks between the total/time line and the errors
92 # line
93 chunk = output[-10000:]
94 lines = chunk.split("\n")
95 lines.pop() # blank line at end
96 # lines[-3] is "Ran NN tests in 0.242s"
97 # lines[-2] is blank
98 # lines[-1] is 'OK' or 'FAILED (failures=1, errors=12)'
99 # or 'FAILED (failures=1)'
100 # or "PASSED (skips=N, successes=N)" (for Twisted-2.0)
101 # there might be other lines dumped here. Scan all the lines.
102 res = {'total': None,
103 'failures': 0,
104 'errors': 0,
105 'skips': 0,
106 'expectedFailures': 0,
107 'unexpectedSuccesses': 0,
109 for l in lines:
110 out = re.search(r'Ran (\d+) tests', l)
111 if out:
112 res['total'] = int(out.group(1))
113 if (l.startswith("OK") or
114 l.startswith("FAILED ") or
115 l.startswith("PASSED")):
116 # the extra space on FAILED_ is to distinguish the overall
117 # status from an individual test which failed. The lack of a
118 # space on the OK is because it may be printed without any
119 # additional text (if there are no skips,etc)
120 out = re.search(r'failures=(\d+)', l)
121 if out: res['failures'] = int(out.group(1))
122 out = re.search(r'errors=(\d+)', l)
123 if out: res['errors'] = int(out.group(1))
124 out = re.search(r'skips=(\d+)', l)
125 if out: res['skips'] = int(out.group(1))
126 out = re.search(r'expectedFailures=(\d+)', l)
127 if out: res['expectedFailures'] = int(out.group(1))
128 out = re.search(r'unexpectedSuccesses=(\d+)', l)
129 if out: res['unexpectedSuccesses'] = int(out.group(1))
130 # successes= is a Twisted-2.0 addition, and is not currently used
131 out = re.search(r'successes=(\d+)', l)
132 if out: res['successes'] = int(out.group(1))
134 return res
137 class TrialTestCaseCounter(LogLineObserver):
138 _line_re = re.compile(r'^(?:Doctest: )?([\w\.]+) \.\.\. \[([^\]]+)\]$')
139 numTests = 0
140 finished = False
142 def outLineReceived(self, line):
143 # different versions of Twisted emit different per-test lines with
144 # the bwverbose reporter.
145 # 2.0.0: testSlave (buildbot.test.test_runner.Create) ... [OK]
146 # 2.1.0: buildbot.test.test_runner.Create.testSlave ... [OK]
147 # 2.4.0: buildbot.test.test_runner.Create.testSlave ... [OK]
148 # Let's just handle the most recent version, since it's the easiest.
149 # Note that doctests create lines line this:
150 # Doctest: viff.field.GF ... [OK]
152 if self.finished:
153 return
154 if line.startswith("=" * 40):
155 self.finished = True
156 return
158 m = self._line_re.search(line.strip())
159 if m:
160 testname, result = m.groups()
161 self.numTests += 1
162 self.step.setProgress('tests', self.numTests)
165 UNSPECIFIED=() # since None is a valid choice
167 class Trial(ShellCommand):
168 """I run a unit test suite using 'trial', a unittest-like testing
169 framework that comes with Twisted. Trial is used to implement Twisted's
170 own unit tests, and is the unittest-framework of choice for many projects
171 that use Twisted internally.
173 Projects that use trial typically have all their test cases in a 'test'
174 subdirectory of their top-level library directory. I.e. for my package
175 'petmail', the tests are in 'petmail/test/test_*.py'. More complicated
176 packages (like Twisted itself) may have multiple test directories, like
177 'twisted/test/test_*.py' for the core functionality and
178 'twisted/mail/test/test_*.py' for the email-specific tests.
180 To run trial tests, you run the 'trial' executable and tell it where the
181 test cases are located. The most common way of doing this is with a
182 module name. For petmail, I would run 'trial petmail.test' and it would
183 locate all the test_*.py files under petmail/test/, running every test
184 case it could find in them. Unlike the unittest.py that comes with
185 Python, you do not run the test_foo.py as a script; you always let trial
186 do the importing and running. The 'tests' parameter controls which tests
187 trial will run: it can be a string or a list of strings.
189 To find these test cases, you must set a PYTHONPATH that allows something
190 like 'import petmail.test' to work. For packages that don't use a
191 separate top-level 'lib' directory, PYTHONPATH=. will work, and will use
192 the test cases (and the code they are testing) in-place.
193 PYTHONPATH=build/lib or PYTHONPATH=build/lib.$ARCH are also useful when
194 you do a'setup.py build' step first. The 'testpath' attribute of this
195 class controls what PYTHONPATH= is set to.
197 Trial has the ability (through the --testmodule flag) to run only the set
198 of test cases named by special 'test-case-name' tags in source files. We
199 can get the list of changed source files from our parent Build and
200 provide them to trial, thus running the minimal set of test cases needed
201 to cover the Changes. This is useful for quick builds, especially in
202 trees with a lot of test cases. The 'testChanges' parameter controls this
203 feature: if set, it will override 'tests'.
205 The trial executable itself is typically just 'trial' (which is usually
206 found on your $PATH as /usr/bin/trial), but it can be overridden with the
207 'trial' parameter. This is useful for Twisted's own unittests, which want
208 to use the copy of bin/trial that comes with the sources. (when bin/trial
209 discovers that it is living in a subdirectory named 'Twisted', it assumes
210 it is being run from the source tree and adds that parent directory to
211 PYTHONPATH. Therefore the canonical way to run Twisted's own unittest
212 suite is './bin/trial twisted.test' rather than 'PYTHONPATH=.
213 /usr/bin/trial twisted.test', especially handy when /usr/bin/trial has
214 not yet been installed).
216 To influence the version of python being used for the tests, or to add
217 flags to the command, set the 'python' parameter. This can be a string
218 (like 'python2.2') or a list (like ['python2.3', '-Wall']).
220 Trial creates and switches into a directory named _trial_temp/ before
221 running the tests, and sends the twisted log (which includes all
222 exceptions) to a file named test.log . This file will be pulled up to
223 the master where it can be seen as part of the status output.
225 There are some class attributes which may be usefully overridden
226 by subclasses. 'trialMode' and 'trialArgs' can influence the trial
227 command line.
230 name = "trial"
231 progressMetrics = ('output', 'tests', 'test.log')
232 # note: the slash only works on unix buildslaves, of course, but we have
233 # no way to know what the buildslave uses as a separator. TODO: figure
234 # out something clever.
235 logfiles = {"test.log": "_trial_temp/test.log"}
236 # we use test.log to track Progress at the end of __init__()
238 flunkOnFailure = True
239 python = None
240 trial = "trial"
241 trialMode = ["--reporter=bwverbose"] # requires Twisted-2.1.0 or newer
242 # for Twisted-2.0.0 or 1.3.0, use ["-o"] instead
243 trialArgs = []
244 testpath = UNSPECIFIED # required (but can be None)
245 testChanges = False # TODO: needs better name
246 recurse = False
247 reactor = None
248 randomly = False
249 tests = None # required
251 def __init__(self, reactor=UNSPECIFIED, python=None, trial=None,
252 testpath=UNSPECIFIED,
253 tests=None, testChanges=None,
254 recurse=None, randomly=None,
255 trialMode=None, trialArgs=None,
256 **kwargs):
258 @type testpath: string
259 @param testpath: use in PYTHONPATH when running the tests. If
260 None, do not set PYTHONPATH. Setting this to '.' will
261 cause the source files to be used in-place.
263 @type python: string (without spaces) or list
264 @param python: which python executable to use. Will form the start of
265 the argv array that will launch trial. If you use this,
266 you should set 'trial' to an explicit path (like
267 /usr/bin/trial or ./bin/trial). Defaults to None, which
268 leaves it out entirely (running 'trial args' instead of
269 'python ./bin/trial args'). Likely values are 'python',
270 ['python2.2'], ['python', '-Wall'], etc.
272 @type trial: string
273 @param trial: which 'trial' executable to run.
274 Defaults to 'trial', which will cause $PATH to be
275 searched and probably find /usr/bin/trial . If you set
276 'python', this should be set to an explicit path (because
277 'python2.3 trial' will not work).
279 @type trialMode: list of strings
280 @param trialMode: a list of arguments to pass to trial, specifically
281 to set the reporting mode. This defaults to ['-to']
282 which means 'verbose colorless output' to the trial
283 that comes with Twisted-2.0.x and at least -2.1.0 .
284 Newer versions of Twisted may come with a trial
285 that prefers ['--reporter=bwverbose'].
287 @type trialArgs: list of strings
288 @param trialArgs: a list of arguments to pass to trial, available to
289 turn on any extra flags you like. Defaults to [].
291 @type tests: list of strings
292 @param tests: a list of test modules to run, like
293 ['twisted.test.test_defer', 'twisted.test.test_process'].
294 If this is a string, it will be converted into a one-item
295 list.
297 @type testChanges: boolean
298 @param testChanges: if True, ignore the 'tests' parameter and instead
299 ask the Build for all the files that make up the
300 Changes going into this build. Pass these filenames
301 to trial and ask it to look for test-case-name
302 tags, running just the tests necessary to cover the
303 changes.
305 @type recurse: boolean
306 @param recurse: If True, pass the --recurse option to trial, allowing
307 test cases to be found in deeper subdirectories of the
308 modules listed in 'tests'. This does not appear to be
309 necessary when using testChanges.
311 @type reactor: string
312 @param reactor: which reactor to use, like 'gtk' or 'java'. If not
313 provided, the Twisted's usual platform-dependent
314 default is used.
316 @type randomly: boolean
317 @param randomly: if True, add the --random=0 argument, which instructs
318 trial to run the unit tests in a random order each
319 time. This occasionally catches problems that might be
320 masked when one module always runs before another
321 (like failing to make registerAdapter calls before
322 lookups are done).
324 @type kwargs: dict
325 @param kwargs: parameters. The following parameters are inherited from
326 L{ShellCommand} and may be useful to set: workdir,
327 haltOnFailure, flunkOnWarnings, flunkOnFailure,
328 warnOnWarnings, warnOnFailure, want_stdout, want_stderr,
329 timeout.
331 ShellCommand.__init__(self, **kwargs)
332 self.addFactoryArguments(reactor=reactor,
333 python=python,
334 trial=trial,
335 testpath=testpath,
336 tests=tests,
337 testChanges=testChanges,
338 recurse=recurse,
339 randomly=randomly,
340 trialMode=trialMode,
341 trialArgs=trialArgs,
344 if python:
345 self.python = python
346 if self.python is not None:
347 if type(self.python) is str:
348 self.python = [self.python]
349 for s in self.python:
350 if " " in s:
351 # this is not strictly an error, but I suspect more
352 # people will accidentally try to use python="python2.3
353 # -Wall" than will use embedded spaces in a python flag
354 log.msg("python= component '%s' has spaces")
355 log.msg("To add -Wall, use python=['python', '-Wall']")
356 why = "python= value has spaces, probably an error"
357 raise ValueError(why)
359 if trial:
360 self.trial = trial
361 if " " in self.trial:
362 raise ValueError("trial= value has spaces")
363 if trialMode is not None:
364 self.trialMode = trialMode
365 if trialArgs is not None:
366 self.trialArgs = trialArgs
368 if testpath is not UNSPECIFIED:
369 self.testpath = testpath
370 if self.testpath is UNSPECIFIED:
371 raise ValueError("You must specify testpath= (it can be None)")
372 assert isinstance(self.testpath, str) or self.testpath is None
374 if reactor is not UNSPECIFIED:
375 self.reactor = reactor
377 if tests is not None:
378 self.tests = tests
379 if type(self.tests) is str:
380 self.tests = [self.tests]
381 if testChanges is not None:
382 self.testChanges = testChanges
383 #self.recurse = True # not sure this is necessary
385 if not self.testChanges and self.tests is None:
386 raise ValueError("Must either set testChanges= or provide tests=")
388 if recurse is not None:
389 self.recurse = recurse
390 if randomly is not None:
391 self.randomly = randomly
393 # build up most of the command, then stash it until start()
394 command = []
395 if self.python:
396 command.extend(self.python)
397 command.append(self.trial)
398 command.extend(self.trialMode)
399 if self.recurse:
400 command.append("--recurse")
401 if self.reactor:
402 command.append("--reactor=%s" % reactor)
403 if self.randomly:
404 command.append("--random=0")
405 command.extend(self.trialArgs)
406 self.command = command
408 if self.reactor:
409 self.description = ["testing", "(%s)" % self.reactor]
410 self.descriptionDone = ["tests"]
411 # commandComplete adds (reactorname) to self.text
412 else:
413 self.description = ["testing"]
414 self.descriptionDone = ["tests"]
416 # this counter will feed Progress along the 'test cases' metric
417 self.addLogObserver('stdio', TrialTestCaseCounter())
418 # this one just measures bytes of output in _trial_temp/test.log
419 self.addLogObserver('test.log', OutputProgressObserver('test.log'))
421 def setupEnvironment(self, cmd):
422 ShellCommand.setupEnvironment(self, cmd)
423 if self.testpath != None:
424 e = cmd.args['env']
425 if e is None:
426 cmd.args['env'] = {'PYTHONPATH': self.testpath}
427 else:
428 # TODO: somehow, each build causes another copy of
429 # self.testpath to get prepended
430 if e.get('PYTHONPATH', "") == "":
431 e['PYTHONPATH'] = self.testpath
432 else:
433 e['PYTHONPATH'] = self.testpath + ":" + e['PYTHONPATH']
434 try:
435 p = cmd.args['env']['PYTHONPATH']
436 if type(p) is not str:
437 log.msg("hey, not a string:", p)
438 assert False
439 except (KeyError, TypeError):
440 # KeyError if args doesn't have ['env']
441 # KeyError if args['env'] doesn't have ['PYTHONPATH']
442 # TypeError if args is None
443 pass
445 def start(self):
446 # now that self.build.allFiles() is nailed down, finish building the
447 # command
448 if self.testChanges:
449 for f in self.build.allFiles():
450 if f.endswith(".py"):
451 self.command.append("--testmodule=%s" % f)
452 else:
453 self.command.extend(self.tests)
454 log.msg("Trial.start: command is", self.command)
456 # if our slave is too old to understand logfiles=, fetch them
457 # manually. This is a fallback for the Twisted buildbot and some old
458 # buildslaves.
459 self._needToPullTestDotLog = False
460 if self.slaveVersionIsOlderThan("shell", "2.1"):
461 log.msg("Trial: buildslave %s is too old to accept logfiles=" %
462 self.getSlaveName())
463 log.msg(" falling back to 'cat _trial_temp/test.log' instead")
464 self.logfiles = {}
465 self._needToPullTestDotLog = True
467 ShellCommand.start(self)
470 def commandComplete(self, cmd):
471 if not self._needToPullTestDotLog:
472 return self._gotTestDotLog(cmd)
474 # if the buildslave was too old, pull test.log now
475 catcmd = ["cat", "_trial_temp/test.log"]
476 c2 = RemoteShellCommand(command=catcmd, workdir=self.workdir)
477 loog = self.addLog("test.log")
478 c2.useLog(loog, True, logfileName="stdio")
479 self.cmd = c2 # to allow interrupts
480 d = c2.run(self, self.remote)
481 d.addCallback(lambda res: self._gotTestDotLog(cmd))
482 return d
484 def rtext(self, fmt='%s'):
485 if self.reactor:
486 rtext = fmt % self.reactor
487 return rtext.replace("reactor", "")
488 return ""
490 def _gotTestDotLog(self, cmd):
491 # figure out all status, then let the various hook functions return
492 # different pieces of it
494 # 'cmd' is the original trial command, so cmd.logs['stdio'] is the
495 # trial output. We don't have access to test.log from here.
496 output = cmd.logs['stdio'].getText()
497 counts = countFailedTests(output)
499 total = counts['total']
500 failures, errors = counts['failures'], counts['errors']
501 parsed = (total != None)
502 text = []
503 text2 = ""
505 if cmd.rc == 0:
506 if parsed:
507 results = SUCCESS
508 if total:
509 text += ["%d %s" % \
510 (total,
511 total == 1 and "test" or "tests"),
512 "passed"]
513 else:
514 text += ["no tests", "run"]
515 else:
516 results = FAILURE
517 text += ["testlog", "unparseable"]
518 text2 = "tests"
519 else:
520 # something failed
521 results = FAILURE
522 if parsed:
523 text.append("tests")
524 if failures:
525 text.append("%d %s" % \
526 (failures,
527 failures == 1 and "failure" or "failures"))
528 if errors:
529 text.append("%d %s" % \
530 (errors,
531 errors == 1 and "error" or "errors"))
532 count = failures + errors
533 text2 = "%d tes%s" % (count, (count == 1 and 't' or 'ts'))
534 else:
535 text += ["tests", "failed"]
536 text2 = "tests"
538 if counts['skips']:
539 text.append("%d %s" % \
540 (counts['skips'],
541 counts['skips'] == 1 and "skip" or "skips"))
542 if counts['expectedFailures']:
543 text.append("%d %s" % \
544 (counts['expectedFailures'],
545 counts['expectedFailures'] == 1 and "todo"
546 or "todos"))
547 if 0: # TODO
548 results = WARNINGS
549 if not text2:
550 text2 = "todo"
552 if 0:
553 # ignore unexpectedSuccesses for now, but it should really mark
554 # the build WARNING
555 if counts['unexpectedSuccesses']:
556 text.append("%d surprises" % counts['unexpectedSuccesses'])
557 results = WARNINGS
558 if not text2:
559 text2 = "tests"
561 if self.reactor:
562 text.append(self.rtext('(%s)'))
563 if text2:
564 text2 = "%s %s" % (text2, self.rtext('(%s)'))
566 self.results = results
567 self.text = text
568 self.text2 = [text2]
570 def addTestResult(self, testname, results, text, tlog):
571 if self.reactor is not None:
572 testname = (self.reactor,) + testname
573 tr = builder.TestResult(testname, results, text, logs={'log': tlog})
574 #self.step_status.build.addTestResult(tr)
575 self.build.build_status.addTestResult(tr)
577 def createSummary(self, loog):
578 output = loog.getText()
579 problems = ""
580 sio = StringIO.StringIO(output)
581 warnings = {}
582 while 1:
583 line = sio.readline()
584 if line == "":
585 break
586 if line.find(" exceptions.DeprecationWarning: ") != -1:
587 # no source
588 warning = line # TODO: consider stripping basedir prefix here
589 warnings[warning] = warnings.get(warning, 0) + 1
590 elif (line.find(" DeprecationWarning: ") != -1 or
591 line.find(" UserWarning: ") != -1):
592 # next line is the source
593 warning = line + sio.readline()
594 warnings[warning] = warnings.get(warning, 0) + 1
595 elif line.find("Warning: ") != -1:
596 warning = line
597 warnings[warning] = warnings.get(warning, 0) + 1
599 if line.find("=" * 60) == 0 or line.find("-" * 60) == 0:
600 problems += line
601 problems += sio.read()
602 break
604 if problems:
605 self.addCompleteLog("problems", problems)
606 # now parse the problems for per-test results
607 pio = StringIO.StringIO(problems)
608 pio.readline() # eat the first separator line
609 testname = None
610 done = False
611 while not done:
612 while 1:
613 line = pio.readline()
614 if line == "":
615 done = True
616 break
617 if line.find("=" * 60) == 0:
618 break
619 if line.find("-" * 60) == 0:
620 # the last case has --- as a separator before the
621 # summary counts are printed
622 done = True
623 break
624 if testname is None:
625 # the first line after the === is like:
626 # EXPECTED FAILURE: testLackOfTB (twisted.test.test_failure.FailureTestCase)
627 # SKIPPED: testRETR (twisted.test.test_ftp.TestFTPServer)
628 # FAILURE: testBatchFile (twisted.conch.test.test_sftp.TestOurServerBatchFile)
629 r = re.search(r'^([^:]+): (\w+) \(([\w\.]+)\)', line)
630 if not r:
631 # TODO: cleanup, if there are no problems,
632 # we hit here
633 continue
634 result, name, case = r.groups()
635 testname = tuple(case.split(".") + [name])
636 results = {'SKIPPED': SKIPPED,
637 'EXPECTED FAILURE': SUCCESS,
638 'UNEXPECTED SUCCESS': WARNINGS,
639 'FAILURE': FAILURE,
640 'ERROR': FAILURE,
641 'SUCCESS': SUCCESS, # not reported
642 }.get(result, WARNINGS)
643 text = result.lower().split()
644 loog = line
645 # the next line is all dashes
646 loog += pio.readline()
647 else:
648 # the rest goes into the log
649 loog += line
650 if testname:
651 self.addTestResult(testname, results, text, loog)
652 testname = None
654 if warnings:
655 lines = warnings.keys()
656 lines.sort()
657 self.addCompleteLog("warnings", "".join(lines))
659 def evaluateCommand(self, cmd):
660 return self.results
662 def getText(self, cmd, results):
663 return self.text
664 def getText2(self, cmd, results):
665 return self.text2
668 class ProcessDocs(ShellCommand):
669 """I build all docs. This requires some LaTeX packages to be installed.
670 It will result in the full documentation book (dvi, pdf, etc).
674 name = "process-docs"
675 warnOnWarnings = 1
676 command = ["admin/process-docs"]
677 description = ["processing", "docs"]
678 descriptionDone = ["docs"]
679 # TODO: track output and time
681 def __init__(self, **kwargs):
683 @type workdir: string
684 @keyword workdir: the workdir to start from: must be the base of the
685 Twisted tree
687 ShellCommand.__init__(self, **kwargs)
689 def createSummary(self, log):
690 output = log.getText()
691 # hlint warnings are of the format: 'WARNING: file:line:col: stuff
692 # latex warnings start with "WARNING: LaTeX Warning: stuff", but
693 # sometimes wrap around to a second line.
694 lines = output.split("\n")
695 warningLines = []
696 wantNext = False
697 for line in lines:
698 wantThis = wantNext
699 wantNext = False
700 if line.startswith("WARNING: "):
701 wantThis = True
702 wantNext = True
703 if wantThis:
704 warningLines.append(line)
706 if warningLines:
707 self.addCompleteLog("warnings", "\n".join(warningLines) + "\n")
708 self.warnings = len(warningLines)
710 def evaluateCommand(self, cmd):
711 if cmd.rc != 0:
712 return FAILURE
713 if self.warnings:
714 return WARNINGS
715 return SUCCESS
717 def getText(self, cmd, results):
718 if results == SUCCESS:
719 return ["docs", "successful"]
720 if results == WARNINGS:
721 return ["docs",
722 "%d warnin%s" % (self.warnings,
723 self.warnings == 1 and 'g' or 'gs')]
724 if results == FAILURE:
725 return ["docs", "failed"]
727 def getText2(self, cmd, results):
728 if results == WARNINGS:
729 return ["%d do%s" % (self.warnings,
730 self.warnings == 1 and 'c' or 'cs')]
731 return ["docs"]
735 class BuildDebs(ShellCommand):
736 """I build the .deb packages."""
738 name = "debuild"
739 flunkOnFailure = 1
740 command = ["debuild", "-uc", "-us"]
741 description = ["building", "debs"]
742 descriptionDone = ["debs"]
744 def __init__(self, **kwargs):
746 @type workdir: string
747 @keyword workdir: the workdir to start from (must be the base of the
748 Twisted tree)
750 ShellCommand.__init__(self, **kwargs)
752 def commandComplete(self, cmd):
753 errors, warnings = 0, 0
754 output = cmd.logs['stdio'].getText()
755 summary = ""
756 sio = StringIO.StringIO(output)
757 for line in sio.readlines():
758 if line.find("E: ") == 0:
759 summary += line
760 errors += 1
761 if line.find("W: ") == 0:
762 summary += line
763 warnings += 1
764 if summary:
765 self.addCompleteLog("problems", summary)
766 self.errors = errors
767 self.warnings = warnings
769 def evaluateCommand(self, cmd):
770 if cmd.rc != 0:
771 return FAILURE
772 if self.errors:
773 return FAILURE
774 if self.warnings:
775 return WARNINGS
776 return SUCCESS
778 def getText(self, cmd, results):
779 text = ["debuild"]
780 if cmd.rc != 0:
781 text.append("failed")
782 errors, warnings = self.errors, self.warnings
783 if warnings or errors:
784 text.append("lintian:")
785 if warnings:
786 text.append("%d warnin%s" % (warnings,
787 warnings == 1 and 'g' or 'gs'))
788 if errors:
789 text.append("%d erro%s" % (errors,
790 errors == 1 and 'r' or 'rs'))
791 return text
793 def getText2(self, cmd, results):
794 if cmd.rc != 0:
795 return ["debuild"]
796 if self.errors or self.warnings:
797 return ["%d lintian" % (self.errors + self.warnings)]
798 return []
800 class RemovePYCs(ShellCommand):
801 name = "remove-.pyc"
802 command = 'find . -name "*.pyc" | xargs rm'
803 description = ["removing", ".pyc", "files"]
804 descriptionDone = ["remove", ".pycs"]