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