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 renderables = ['tests']
196 flunkOnFailure = True
197 python = None
198 trial = "trial"
199 trialMode = ["--reporter=bwverbose"]
200
201 trialArgs = []
202 testpath = UNSPECIFIED
203 testChanges = False
204 recurse = False
205 reactor = None
206 randomly = False
207 tests = None
208
209 - def __init__(self, reactor=UNSPECIFIED, python=None, trial=None,
210 testpath=UNSPECIFIED,
211 tests=None, testChanges=None,
212 recurse=None, randomly=None,
213 trialMode=None, trialArgs=None,
214 **kwargs):
215 """
216 @type testpath: string
217 @param testpath: use in PYTHONPATH when running the tests. If
218 None, do not set PYTHONPATH. Setting this to '.' will
219 cause the source files to be used in-place.
220
221 @type python: string (without spaces) or list
222 @param python: which python executable to use. Will form the start of
223 the argv array that will launch trial. If you use this,
224 you should set 'trial' to an explicit path (like
225 /usr/bin/trial or ./bin/trial). Defaults to None, which
226 leaves it out entirely (running 'trial args' instead of
227 'python ./bin/trial args'). Likely values are 'python',
228 ['python2.2'], ['python', '-Wall'], etc.
229
230 @type trial: string
231 @param trial: which 'trial' executable to run.
232 Defaults to 'trial', which will cause $PATH to be
233 searched and probably find /usr/bin/trial . If you set
234 'python', this should be set to an explicit path (because
235 'python2.3 trial' will not work).
236
237 @type trialMode: list of strings
238 @param trialMode: a list of arguments to pass to trial, specifically
239 to set the reporting mode. This defaults to ['-to']
240 which means 'verbose colorless output' to the trial
241 that comes with Twisted-2.0.x and at least -2.1.0 .
242 Newer versions of Twisted may come with a trial
243 that prefers ['--reporter=bwverbose'].
244
245 @type trialArgs: list of strings
246 @param trialArgs: a list of arguments to pass to trial, available to
247 turn on any extra flags you like. Defaults to [].
248
249 @type tests: list of strings
250 @param tests: a list of test modules to run, like
251 ['twisted.test.test_defer', 'twisted.test.test_process'].
252 If this is a string, it will be converted into a one-item
253 list.
254
255 @type testChanges: boolean
256 @param testChanges: if True, ignore the 'tests' parameter and instead
257 ask the Build for all the files that make up the
258 Changes going into this build. Pass these filenames
259 to trial and ask it to look for test-case-name
260 tags, running just the tests necessary to cover the
261 changes.
262
263 @type recurse: boolean
264 @param recurse: If True, pass the --recurse option to trial, allowing
265 test cases to be found in deeper subdirectories of the
266 modules listed in 'tests'. This does not appear to be
267 necessary when using testChanges.
268
269 @type reactor: string
270 @param reactor: which reactor to use, like 'gtk' or 'java'. If not
271 provided, the Twisted's usual platform-dependent
272 default is used.
273
274 @type randomly: boolean
275 @param randomly: if True, add the --random=0 argument, which instructs
276 trial to run the unit tests in a random order each
277 time. This occasionally catches problems that might be
278 masked when one module always runs before another
279 (like failing to make registerAdapter calls before
280 lookups are done).
281
282 @type kwargs: dict
283 @param kwargs: parameters. The following parameters are inherited from
284 L{ShellCommand} and may be useful to set: workdir,
285 haltOnFailure, flunkOnWarnings, flunkOnFailure,
286 warnOnWarnings, warnOnFailure, want_stdout, want_stderr,
287 timeout.
288 """
289 ShellCommand.__init__(self, **kwargs)
290 self.addFactoryArguments(reactor=reactor,
291 python=python,
292 trial=trial,
293 testpath=testpath,
294 tests=tests,
295 testChanges=testChanges,
296 recurse=recurse,
297 randomly=randomly,
298 trialMode=trialMode,
299 trialArgs=trialArgs,
300 )
301
302 if python:
303 self.python = python
304 if self.python is not None:
305 if type(self.python) is str:
306 self.python = [self.python]
307 for s in self.python:
308 if " " in s:
309
310
311
312 log.msg("python= component '%s' has spaces")
313 log.msg("To add -Wall, use python=['python', '-Wall']")
314 why = "python= value has spaces, probably an error"
315 raise ValueError(why)
316
317 if trial:
318 self.trial = trial
319 if " " in self.trial:
320 raise ValueError("trial= value has spaces")
321 if trialMode is not None:
322 self.trialMode = trialMode
323 if trialArgs is not None:
324 self.trialArgs = trialArgs
325
326 if testpath is not UNSPECIFIED:
327 self.testpath = testpath
328 if self.testpath is UNSPECIFIED:
329 raise ValueError("You must specify testpath= (it can be None)")
330 assert isinstance(self.testpath, str) or self.testpath is None
331
332 if reactor is not UNSPECIFIED:
333 self.reactor = reactor
334
335 if tests is not None:
336 self.tests = tests
337 if type(self.tests) is str:
338 self.tests = [self.tests]
339 if testChanges is not None:
340 self.testChanges = testChanges
341
342
343 if not self.testChanges and self.tests is None:
344 raise ValueError("Must either set testChanges= or provide tests=")
345
346 if recurse is not None:
347 self.recurse = recurse
348 if randomly is not None:
349 self.randomly = randomly
350
351
352 command = []
353 if self.python:
354 command.extend(self.python)
355 command.append(self.trial)
356 command.extend(self.trialMode)
357 if self.recurse:
358 command.append("--recurse")
359 if self.reactor:
360 command.append("--reactor=%s" % reactor)
361 if self.randomly:
362 command.append("--random=0")
363 command.extend(self.trialArgs)
364 self.command = command
365
366 if self.reactor:
367 self.description = ["testing", "(%s)" % self.reactor]
368 self.descriptionDone = ["tests"]
369
370 else:
371 self.description = ["testing"]
372 self.descriptionDone = ["tests"]
373
374
375 self.addLogObserver('stdio', TrialTestCaseCounter())
376
377 self.addLogObserver('test.log', OutputProgressObserver('test.log'))
378
394
407
408
410
411
412
413
414
415 output = cmd.logs['stdio'].getText()
416 counts = countFailedTests(output)
417
418 total = counts['total']
419 failures, errors = counts['failures'], counts['errors']
420 parsed = (total != None)
421 text = []
422 text2 = ""
423
424 if cmd.rc == 0:
425 if parsed:
426 results = SUCCESS
427 if total:
428 text += ["%d %s" % \
429 (total,
430 total == 1 and "test" or "tests"),
431 "passed"]
432 else:
433 text += ["no tests", "run"]
434 else:
435 results = FAILURE
436 text += ["testlog", "unparseable"]
437 text2 = "tests"
438 else:
439
440 results = FAILURE
441 if parsed:
442 text.append("tests")
443 if failures:
444 text.append("%d %s" % \
445 (failures,
446 failures == 1 and "failure" or "failures"))
447 if errors:
448 text.append("%d %s" % \
449 (errors,
450 errors == 1 and "error" or "errors"))
451 count = failures + errors
452 text2 = "%d tes%s" % (count, (count == 1 and 't' or 'ts'))
453 else:
454 text += ["tests", "failed"]
455 text2 = "tests"
456
457 if counts['skips']:
458 text.append("%d %s" % \
459 (counts['skips'],
460 counts['skips'] == 1 and "skip" or "skips"))
461 if counts['expectedFailures']:
462 text.append("%d %s" % \
463 (counts['expectedFailures'],
464 counts['expectedFailures'] == 1 and "todo"
465 or "todos"))
466 if 0:
467 results = WARNINGS
468 if not text2:
469 text2 = "todo"
470
471 if 0:
472
473
474 if counts['unexpectedSuccesses']:
475 text.append("%d surprises" % counts['unexpectedSuccesses'])
476 results = WARNINGS
477 if not text2:
478 text2 = "tests"
479
480 if self.reactor:
481 text.append(self.rtext('(%s)'))
482 if text2:
483 text2 = "%s %s" % (text2, self.rtext('(%s)'))
484
485 self.results = results
486 self.text = text
487 self.text2 = [text2]
488
489
490 - def rtext(self, fmt='%s'):
491 if self.reactor:
492 rtext = fmt % self.reactor
493 return rtext.replace("reactor", "")
494 return ""
495
502
504 output = loog.getText()
505 problems = ""
506 sio = StringIO.StringIO(output)
507 warnings = {}
508 while 1:
509 line = sio.readline()
510 if line == "":
511 break
512 if line.find(" exceptions.DeprecationWarning: ") != -1:
513
514 warning = line
515 warnings[warning] = warnings.get(warning, 0) + 1
516 elif (line.find(" DeprecationWarning: ") != -1 or
517 line.find(" UserWarning: ") != -1):
518
519 warning = line + sio.readline()
520 warnings[warning] = warnings.get(warning, 0) + 1
521 elif line.find("Warning: ") != -1:
522 warning = line
523 warnings[warning] = warnings.get(warning, 0) + 1
524
525 if line.find("=" * 60) == 0 or line.find("-" * 60) == 0:
526 problems += line
527 problems += sio.read()
528 break
529
530 if problems:
531 self.addCompleteLog("problems", problems)
532
533 pio = StringIO.StringIO(problems)
534 pio.readline()
535 testname = None
536 done = False
537 while not done:
538 while 1:
539 line = pio.readline()
540 if line == "":
541 done = True
542 break
543 if line.find("=" * 60) == 0:
544 break
545 if line.find("-" * 60) == 0:
546
547
548 done = True
549 break
550 if testname is None:
551
552
553
554
555 r = re.search(r'^([^:]+): (\w+) \(([\w\.]+)\)', line)
556 if not r:
557
558
559 continue
560 result, name, case = r.groups()
561 testname = tuple(case.split(".") + [name])
562 results = {'SKIPPED': SKIPPED,
563 'EXPECTED FAILURE': SUCCESS,
564 'UNEXPECTED SUCCESS': WARNINGS,
565 'FAILURE': FAILURE,
566 'ERROR': FAILURE,
567 'SUCCESS': SUCCESS,
568 }.get(result, WARNINGS)
569 text = result.lower().split()
570 loog = line
571
572 loog += pio.readline()
573 else:
574
575 loog += line
576 if testname:
577 self.addTestResult(testname, results, text, loog)
578 testname = None
579
580 if warnings:
581 lines = warnings.keys()
582 lines.sort()
583 self.addCompleteLog("warnings", "".join(lines))
584
587
588 - def getText(self, cmd, results):
590 - def getText2(self, cmd, results):
592
593
595 name = "remove-.pyc"
596 command = ['find', '.', '-name', '*.pyc', '-exec', 'rm', '{}', ';']
597 description = ["removing", ".pyc", "files"]
598 descriptionDone = ["remove", ".pycs"]
599