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 builder
20 from buildbot.status.builder import SUCCESS, FAILURE, WARNINGS, SKIPPED
21 from buildbot.process.buildstep import LogLineObserver, OutputProgressObserver
22 from buildbot.process.buildstep import RemoteShellCommand
23 from buildbot.steps.shell import ShellCommand
24
25 try:
26 import cStringIO
27 StringIO = cStringIO
28 except ImportError:
29 import StringIO
30 import re
31
32
33
34 -class HLint(ShellCommand):
35 """I run a 'lint' checker over a set of .xhtml files. Any deviations
36 from recommended style is flagged and put in the output log.
37
38 This step looks at .changes in the parent Build to extract a list of
39 Lore XHTML files to check."""
40
41 name = "hlint"
42 description = ["running", "hlint"]
43 descriptionDone = ["hlint"]
44 warnOnWarnings = True
45 warnOnFailure = True
46
47 warnings = 0
48
49 - def __init__(self, python=None, **kwargs):
53
55
56 htmlFiles = {}
57 for f in self.build.allFiles():
58 if f.endswith(".xhtml") and not f.startswith("sandbox/"):
59 htmlFiles[f] = 1
60
61 hlintTargets = htmlFiles.keys()
62 hlintTargets.sort()
63 if not hlintTargets:
64 return SKIPPED
65 self.hlintFiles = hlintTargets
66 c = []
67 if self.python:
68 c.append(self.python)
69 c += ["bin/lore", "-p", "--output", "lint"] + self.hlintFiles
70 self.setCommand(c)
71
72
73 self.addCompleteLog("files", "\n".join(self.hlintFiles)+"\n")
74
75 ShellCommand.start(self)
76
88
96
97 - def getText2(self, cmd, results):
98 if cmd.rc != 0:
99 return ["hlint"]
100 return ["%d hlin%s" % (self.warnings,
101 self.warnings == 1 and 't' or 'ts')]
102
104
105
106
107 chunk = output[-10000:]
108 lines = chunk.split("\n")
109 lines.pop()
110
111
112
113
114
115
116 res = {'total': None,
117 'failures': 0,
118 'errors': 0,
119 'skips': 0,
120 'expectedFailures': 0,
121 'unexpectedSuccesses': 0,
122 }
123 for l in lines:
124 out = re.search(r'Ran (\d+) tests', l)
125 if out:
126 res['total'] = int(out.group(1))
127 if (l.startswith("OK") or
128 l.startswith("FAILED ") or
129 l.startswith("PASSED")):
130
131
132
133
134 out = re.search(r'failures=(\d+)', l)
135 if out: res['failures'] = int(out.group(1))
136 out = re.search(r'errors=(\d+)', l)
137 if out: res['errors'] = int(out.group(1))
138 out = re.search(r'skips=(\d+)', l)
139 if out: res['skips'] = int(out.group(1))
140 out = re.search(r'expectedFailures=(\d+)', l)
141 if out: res['expectedFailures'] = int(out.group(1))
142 out = re.search(r'unexpectedSuccesses=(\d+)', l)
143 if out: res['unexpectedSuccesses'] = int(out.group(1))
144
145 out = re.search(r'successes=(\d+)', l)
146 if out: res['successes'] = int(out.group(1))
147
148 return res
149
150
152 _line_re = re.compile(r'^(?:Doctest: )?([\w\.]+) \.\.\. \[([^\]]+)\]$')
153 numTests = 0
154 finished = False
155
157
158
159
160
161
162
163
164
165
166 if self.finished:
167 return
168 if line.startswith("=" * 40):
169 self.finished = True
170 return
171
172 m = self._line_re.search(line.strip())
173 if m:
174 testname, result = m.groups()
175 self.numTests += 1
176 self.step.setProgress('tests', self.numTests)
177
178
179 UNSPECIFIED=()
180
181 -class Trial(ShellCommand):
182 """
183 There are some class attributes which may be usefully overridden
184 by subclasses. 'trialMode' and 'trialArgs' can influence the trial
185 command line.
186 """
187
188 name = "trial"
189 progressMetrics = ('output', 'tests', 'test.log')
190
191
192
193 logfiles = {"test.log": "_trial_temp/test.log"}
194
195
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
380 ShellCommand.setupEnvironment(self, cmd)
381 if self.testpath != None:
382 e = cmd.args['env']
383 if e is None:
384 cmd.args['env'] = {'PYTHONPATH': self.testpath}
385 else:
386
387
388 if e.get('PYTHONPATH', "") == "":
389 e['PYTHONPATH'] = self.testpath
390 else:
391 e['PYTHONPATH'] = self.testpath + ":" + e['PYTHONPATH']
392 try:
393 p = cmd.args['env']['PYTHONPATH']
394 if type(p) is not str:
395 log.msg("hey, not a string:", p)
396 assert False
397 except (KeyError, TypeError):
398
399
400
401 pass
402
404
405
406 if self.testChanges:
407 for f in self.build.allFiles():
408 if f.endswith(".py"):
409 self.command.append("--testmodule=%s" % f)
410 else:
411 self.command.extend(self.tests)
412 log.msg("Trial.start: command is", self.command)
413
414
415
416
417 self._needToPullTestDotLog = False
418 if self.slaveVersionIsOlderThan("shell", "2.1"):
419 log.msg("Trial: buildslave %s is too old to accept logfiles=" %
420 self.getSlaveName())
421 log.msg(" falling back to 'cat _trial_temp/test.log' instead")
422 self.logfiles = {}
423 self._needToPullTestDotLog = True
424
425 ShellCommand.start(self)
426
427
429 if not self._needToPullTestDotLog:
430 return self._gotTestDotLog(cmd)
431
432
433 catcmd = ["cat", "_trial_temp/test.log"]
434 c2 = RemoteShellCommand(command=catcmd, workdir=self.workdir)
435 loog = self.addLog("test.log")
436 c2.useLog(loog, True, logfileName="stdio")
437 self.cmd = c2
438 d = c2.run(self, self.remote)
439 d.addCallback(lambda res: self._gotTestDotLog(cmd))
440 return d
441
442 - def rtext(self, fmt='%s'):
443 if self.reactor:
444 rtext = fmt % self.reactor
445 return rtext.replace("reactor", "")
446 return ""
447
449
450
451
452
453
454 output = cmd.logs['stdio'].getText()
455 counts = countFailedTests(output)
456
457 total = counts['total']
458 failures, errors = counts['failures'], counts['errors']
459 parsed = (total != None)
460 text = []
461 text2 = ""
462
463 if cmd.rc == 0:
464 if parsed:
465 results = SUCCESS
466 if total:
467 text += ["%d %s" % \
468 (total,
469 total == 1 and "test" or "tests"),
470 "passed"]
471 else:
472 text += ["no tests", "run"]
473 else:
474 results = FAILURE
475 text += ["testlog", "unparseable"]
476 text2 = "tests"
477 else:
478
479 results = FAILURE
480 if parsed:
481 text.append("tests")
482 if failures:
483 text.append("%d %s" % \
484 (failures,
485 failures == 1 and "failure" or "failures"))
486 if errors:
487 text.append("%d %s" % \
488 (errors,
489 errors == 1 and "error" or "errors"))
490 count = failures + errors
491 text2 = "%d tes%s" % (count, (count == 1 and 't' or 'ts'))
492 else:
493 text += ["tests", "failed"]
494 text2 = "tests"
495
496 if counts['skips']:
497 text.append("%d %s" % \
498 (counts['skips'],
499 counts['skips'] == 1 and "skip" or "skips"))
500 if counts['expectedFailures']:
501 text.append("%d %s" % \
502 (counts['expectedFailures'],
503 counts['expectedFailures'] == 1 and "todo"
504 or "todos"))
505 if 0:
506 results = WARNINGS
507 if not text2:
508 text2 = "todo"
509
510 if 0:
511
512
513 if counts['unexpectedSuccesses']:
514 text.append("%d surprises" % counts['unexpectedSuccesses'])
515 results = WARNINGS
516 if not text2:
517 text2 = "tests"
518
519 if self.reactor:
520 text.append(self.rtext('(%s)'))
521 if text2:
522 text2 = "%s %s" % (text2, self.rtext('(%s)'))
523
524 self.results = results
525 self.text = text
526 self.text2 = [text2]
527
534
536 output = loog.getText()
537 problems = ""
538 sio = StringIO.StringIO(output)
539 warnings = {}
540 while 1:
541 line = sio.readline()
542 if line == "":
543 break
544 if line.find(" exceptions.DeprecationWarning: ") != -1:
545
546 warning = line
547 warnings[warning] = warnings.get(warning, 0) + 1
548 elif (line.find(" DeprecationWarning: ") != -1 or
549 line.find(" UserWarning: ") != -1):
550
551 warning = line + sio.readline()
552 warnings[warning] = warnings.get(warning, 0) + 1
553 elif line.find("Warning: ") != -1:
554 warning = line
555 warnings[warning] = warnings.get(warning, 0) + 1
556
557 if line.find("=" * 60) == 0 or line.find("-" * 60) == 0:
558 problems += line
559 problems += sio.read()
560 break
561
562 if problems:
563 self.addCompleteLog("problems", problems)
564
565 pio = StringIO.StringIO(problems)
566 pio.readline()
567 testname = None
568 done = False
569 while not done:
570 while 1:
571 line = pio.readline()
572 if line == "":
573 done = True
574 break
575 if line.find("=" * 60) == 0:
576 break
577 if line.find("-" * 60) == 0:
578
579
580 done = True
581 break
582 if testname is None:
583
584
585
586
587 r = re.search(r'^([^:]+): (\w+) \(([\w\.]+)\)', line)
588 if not r:
589
590
591 continue
592 result, name, case = r.groups()
593 testname = tuple(case.split(".") + [name])
594 results = {'SKIPPED': SKIPPED,
595 'EXPECTED FAILURE': SUCCESS,
596 'UNEXPECTED SUCCESS': WARNINGS,
597 'FAILURE': FAILURE,
598 'ERROR': FAILURE,
599 'SUCCESS': SUCCESS,
600 }.get(result, WARNINGS)
601 text = result.lower().split()
602 loog = line
603
604 loog += pio.readline()
605 else:
606
607 loog += line
608 if testname:
609 self.addTestResult(testname, results, text, loog)
610 testname = None
611
612 if warnings:
613 lines = warnings.keys()
614 lines.sort()
615 self.addCompleteLog("warnings", "".join(lines))
616
619
620 - def getText(self, cmd, results):
622 - def getText2(self, cmd, results):
624
625
627 name = "remove-.pyc"
628 command = ['find', '.', '-name', '*.pyc', '-exec', 'rm', '{}', ';']
629 description = ["removing", ".pyc", "files"]
630 descriptionDone = ["remove", ".pycs"]
631