OLD | NEW |
| (Empty) |
1 # -*- test-case-name: twisted.trial.test.test_script -*- | |
2 | |
3 # Copyright (c) 2001-2007 Twisted Matrix Laboratories. | |
4 # See LICENSE for details. | |
5 | |
6 | |
7 import sys, os, random, gc, time, warnings | |
8 | |
9 from twisted.internet import defer | |
10 from twisted.application import app | |
11 from twisted.python import usage, reflect, failure | |
12 from twisted import plugin | |
13 from twisted.python.util import spewer | |
14 from twisted.python.compat import set | |
15 from twisted.trial import runner, itrial, reporter | |
16 | |
17 | |
18 # Yea, this is stupid. Leave it for for command-line compatibility for a | |
19 # while, though. | |
20 TBFORMAT_MAP = { | |
21 'plain': 'default', | |
22 'default': 'default', | |
23 'emacs': 'brief', | |
24 'brief': 'brief', | |
25 'cgitb': 'verbose', | |
26 'verbose': 'verbose' | |
27 } | |
28 | |
29 | |
30 def _parseLocalVariables(line): | |
31 """Accepts a single line in Emacs local variable declaration format and | |
32 returns a dict of all the variables {name: value}. | |
33 Raises ValueError if 'line' is in the wrong format. | |
34 | |
35 See http://www.gnu.org/software/emacs/manual/html_node/File-Variables.html | |
36 """ | |
37 paren = '-*-' | |
38 start = line.find(paren) + len(paren) | |
39 end = line.rfind(paren) | |
40 if start == -1 or end == -1: | |
41 raise ValueError("%r not a valid local variable declaration" % (line,)) | |
42 items = line[start:end].split(';') | |
43 localVars = {} | |
44 for item in items: | |
45 if len(item.strip()) == 0: | |
46 continue | |
47 split = item.split(':') | |
48 if len(split) != 2: | |
49 raise ValueError("%r contains invalid declaration %r" | |
50 % (line, item)) | |
51 localVars[split[0].strip()] = split[1].strip() | |
52 return localVars | |
53 | |
54 | |
55 def loadLocalVariables(filename): | |
56 """Accepts a filename and attempts to load the Emacs variable declarations | |
57 from that file, simulating what Emacs does. | |
58 | |
59 See http://www.gnu.org/software/emacs/manual/html_node/File-Variables.html | |
60 """ | |
61 f = file(filename, "r") | |
62 lines = [f.readline(), f.readline()] | |
63 f.close() | |
64 for line in lines: | |
65 try: | |
66 return _parseLocalVariables(line) | |
67 except ValueError: | |
68 pass | |
69 return {} | |
70 | |
71 | |
72 def getTestModules(filename): | |
73 testCaseVar = loadLocalVariables(filename).get('test-case-name', None) | |
74 if testCaseVar is None: | |
75 return [] | |
76 return testCaseVar.split(',') | |
77 | |
78 | |
79 def isTestFile(filename): | |
80 """Returns true if 'filename' looks like a file containing unit tests. | |
81 False otherwise. Doesn't care whether filename exists. | |
82 """ | |
83 basename = os.path.basename(filename) | |
84 return (basename.startswith('test_') | |
85 and os.path.splitext(basename)[1] == ('.py')) | |
86 | |
87 | |
88 def _zshReporterAction(): | |
89 return "(%s)" % (" ".join([p.longOpt for p in plugin.getPlugins(itrial.IRepo
rter)]),) | |
90 | |
91 class Options(usage.Options, app.ReactorSelectionMixin): | |
92 synopsis = """%s [options] [[file|package|module|TestCase|testmethod]...] | |
93 """ % (os.path.basename(sys.argv[0]),) | |
94 | |
95 optFlags = [["help", "h"], | |
96 ["rterrors", "e", "realtime errors, print out tracebacks as " | |
97 "soon as they occur"], | |
98 ["debug", "b", "Run tests in the Python debugger. Will load " | |
99 "'.pdbrc' from current directory if it exists."], | |
100 ["debug-stacktraces", "B", "Report Deferred creation and " | |
101 "callback stack traces"], | |
102 ["nopm", None, "don't automatically jump into debugger for " | |
103 "postmorteming of exceptions"], | |
104 ["dry-run", 'n', "do everything but run the tests"], | |
105 ["force-gc", None, "Have Trial run gc.collect() before and " | |
106 "after each test case."], | |
107 ["profile", None, "Run tests under the Python profiler"], | |
108 ["unclean-warnings", None, | |
109 "Turn dirty reactor errors into warnings"], | |
110 ["until-failure", "u", "Repeat test until it fails"], | |
111 ["no-recurse", "N", "Don't recurse into packages"], | |
112 ['help-reporters', None, | |
113 "Help on available output plugins (reporters)"] | |
114 ] | |
115 | |
116 optParameters = [ | |
117 ["logfile", "l", "test.log", "log file name"], | |
118 ["random", "z", None, | |
119 "Run tests in random order using the specified seed"], | |
120 ['temp-directory', None, '_trial_temp', | |
121 'Path to use as working directory for tests.'], | |
122 ['reporter', None, 'verbose', | |
123 'The reporter to use for this test run. See --help-reporters for ' | |
124 'more info.']] | |
125 | |
126 zsh_actions = {"tbformat":"(plain emacs cgitb)", | |
127 "reporter":_zshReporterAction} | |
128 zsh_actionDescr = {"logfile":"log file name", | |
129 "random":"random seed"} | |
130 zsh_extras = ["*:file|module|package|TestCase|testMethod:_files -g '*.py'"] | |
131 | |
132 fallbackReporter = reporter.TreeReporter | |
133 extra = None | |
134 tracer = None | |
135 | |
136 def __init__(self): | |
137 self['tests'] = set() | |
138 usage.Options.__init__(self) | |
139 | |
140 def opt_coverage(self): | |
141 """ | |
142 Generate coverage information in the _trial_temp/coverage. Requires | |
143 Python 2.3.3. | |
144 """ | |
145 coverdir = 'coverage' | |
146 print "Setting coverage directory to %s." % (coverdir,) | |
147 import trace | |
148 | |
149 # begin monkey patch --------------------------- | |
150 # Before Python 2.4, this function asserted that 'filename' had | |
151 # to end with '.py' This is wrong for at least two reasons: | |
152 # 1. We might be wanting to find executable line nos in a script | |
153 # 2. The implementation should use os.splitext | |
154 # This monkey patch is the same function as in the stdlib (v2.3) | |
155 # but with the assertion removed. | |
156 def find_executable_linenos(filename): | |
157 """Return dict where keys are line numbers in the line number | |
158 table. | |
159 """ | |
160 #assert filename.endswith('.py') # YOU BASTARDS | |
161 try: | |
162 prog = open(filename).read() | |
163 prog = '\n'.join(prog.splitlines()) + '\n' | |
164 except IOError, err: | |
165 sys.stderr.write("Not printing coverage data for %r: %s\n" | |
166 % (filename, err)) | |
167 sys.stderr.flush() | |
168 return {} | |
169 code = compile(prog, filename, "exec") | |
170 strs = trace.find_strings(filename) | |
171 return trace.find_lines(code, strs) | |
172 | |
173 trace.find_executable_linenos = find_executable_linenos | |
174 # end monkey patch ------------------------------ | |
175 | |
176 self.coverdir = os.path.abspath(os.path.join(self['temp-directory'], cov
erdir)) | |
177 self.tracer = trace.Trace(count=1, trace=0) | |
178 sys.settrace(self.tracer.globaltrace) | |
179 | |
180 def opt_testmodule(self, filename): | |
181 "Filename to grep for test cases (-*- test-case-name)" | |
182 # If the filename passed to this parameter looks like a test module | |
183 # we just add that to the test suite. | |
184 # | |
185 # If not, we inspect it for an Emacs buffer local variable called | |
186 # 'test-case-name'. If that variable is declared, we try to add its | |
187 # value to the test suite as a module. | |
188 # | |
189 # This parameter allows automated processes (like Buildbot) to pass | |
190 # a list of files to Trial with the general expectation of "these files, | |
191 # whatever they are, will get tested" | |
192 if not os.path.isfile(filename): | |
193 sys.stderr.write("File %r doesn't exist\n" % (filename,)) | |
194 return | |
195 filename = os.path.abspath(filename) | |
196 if isTestFile(filename): | |
197 self['tests'].add(filename) | |
198 else: | |
199 self['tests'].update(getTestModules(filename)) | |
200 | |
201 def opt_spew(self): | |
202 """Print an insanely verbose log of everything that happens. Useful | |
203 when debugging freezes or locks in complex code.""" | |
204 sys.settrace(spewer) | |
205 | |
206 | |
207 def opt_help_reporters(self): | |
208 synopsis = ("Trial's output can be customized using plugins called " | |
209 "Reporters. You can\nselect any of the following " | |
210 "reporters using --reporter=<foo>\n") | |
211 print synopsis | |
212 for p in plugin.getPlugins(itrial.IReporter): | |
213 print ' ', p.longOpt, '\t', p.description | |
214 print | |
215 sys.exit(0) | |
216 | |
217 def opt_disablegc(self): | |
218 """Disable the garbage collector""" | |
219 gc.disable() | |
220 | |
221 def opt_tbformat(self, opt): | |
222 """Specify the format to display tracebacks with. Valid formats are | |
223 'plain', 'emacs', and 'cgitb' which uses the nicely verbose stdlib | |
224 cgitb.text function""" | |
225 try: | |
226 self['tbformat'] = TBFORMAT_MAP[opt] | |
227 except KeyError: | |
228 raise usage.UsageError( | |
229 "tbformat must be 'plain', 'emacs', or 'cgitb'.") | |
230 | |
231 def opt_extra(self, arg): | |
232 """ | |
233 Add an extra argument. (This is a hack necessary for interfacing with | |
234 emacs's `gud'.) | |
235 """ | |
236 if self.extra is None: | |
237 self.extra = [] | |
238 self.extra.append(arg) | |
239 opt_x = opt_extra | |
240 | |
241 def opt_recursionlimit(self, arg): | |
242 """see sys.setrecursionlimit()""" | |
243 try: | |
244 sys.setrecursionlimit(int(arg)) | |
245 except (TypeError, ValueError): | |
246 raise usage.UsageError( | |
247 "argument to recursionlimit must be an integer") | |
248 | |
249 def opt_random(self, option): | |
250 try: | |
251 self['random'] = long(option) | |
252 except ValueError: | |
253 raise usage.UsageError( | |
254 "Argument to --random must be a positive integer") | |
255 else: | |
256 if self['random'] < 0: | |
257 raise usage.UsageError( | |
258 "Argument to --random must be a positive integer") | |
259 elif self['random'] == 0: | |
260 self['random'] = long(time.time() * 100) | |
261 | |
262 def opt_without_module(self, option): | |
263 """ | |
264 Fake the lack of the specified modules, separated with commas. | |
265 """ | |
266 for module in option.split(","): | |
267 if module in sys.modules: | |
268 warnings.warn("Module '%s' already imported, " | |
269 "disabling anyway." % (module,), | |
270 category=RuntimeWarning) | |
271 sys.modules[module] = None | |
272 | |
273 def parseArgs(self, *args): | |
274 self['tests'].update(args) | |
275 if self.extra is not None: | |
276 self['tests'].update(self.extra) | |
277 | |
278 def _loadReporterByName(self, name): | |
279 for p in plugin.getPlugins(itrial.IReporter): | |
280 qual = "%s.%s" % (p.module, p.klass) | |
281 if p.longOpt == name: | |
282 return reflect.namedAny(qual) | |
283 raise usage.UsageError("Only pass names of Reporter plugins to " | |
284 "--reporter. See --help-reporters for " | |
285 "more info.") | |
286 | |
287 | |
288 def postOptions(self): | |
289 | |
290 # Only load reporters now, as opposed to any earlier, to avoid letting | |
291 # application-defined plugins muck up reactor selecting by importing | |
292 # t.i.reactor and causing the default to be installed. | |
293 self['reporter'] = self._loadReporterByName(self['reporter']) | |
294 | |
295 if 'tbformat' not in self: | |
296 self['tbformat'] = 'default' | |
297 if self['nopm']: | |
298 if not self['debug']: | |
299 raise usage.UsageError("you must specify --debug when using " | |
300 "--nopm ") | |
301 failure.DO_POST_MORTEM = False | |
302 | |
303 | |
304 def _initialDebugSetup(config): | |
305 # do this part of debug setup first for easy debugging of import failures | |
306 if config['debug']: | |
307 failure.startDebugMode() | |
308 if config['debug'] or config['debug-stacktraces']: | |
309 defer.setDebugging(True) | |
310 | |
311 | |
312 def _getSuite(config): | |
313 loader = _getLoader(config) | |
314 recurse = not config['no-recurse'] | |
315 return loader.loadByNames(config['tests'], recurse) | |
316 | |
317 | |
318 def _getLoader(config): | |
319 loader = runner.TestLoader() | |
320 if config['random']: | |
321 randomer = random.Random() | |
322 randomer.seed(config['random']) | |
323 loader.sorter = lambda x : randomer.random() | |
324 print 'Running tests shuffled with seed %d\n' % config['random'] | |
325 if not config['until-failure']: | |
326 loader.suiteFactory = runner.DestructiveTestSuite | |
327 return loader | |
328 | |
329 | |
330 def _makeRunner(config): | |
331 mode = None | |
332 if config['debug']: | |
333 mode = runner.TrialRunner.DEBUG | |
334 if config['dry-run']: | |
335 mode = runner.TrialRunner.DRY_RUN | |
336 return runner.TrialRunner(config['reporter'], | |
337 mode=mode, | |
338 profile=config['profile'], | |
339 logfile=config['logfile'], | |
340 tracebackFormat=config['tbformat'], | |
341 realTimeErrors=config['rterrors'], | |
342 uncleanWarnings=config['unclean-warnings'], | |
343 workingDirectory=config['temp-directory'], | |
344 forceGarbageCollection=config['force-gc']) | |
345 | |
346 | |
347 def run(): | |
348 if len(sys.argv) == 1: | |
349 sys.argv.append("--help") | |
350 config = Options() | |
351 try: | |
352 config.parseOptions() | |
353 except usage.error, ue: | |
354 raise SystemExit, "%s: %s" % (sys.argv[0], ue) | |
355 _initialDebugSetup(config) | |
356 trialRunner = _makeRunner(config) | |
357 suite = _getSuite(config) | |
358 if config['until-failure']: | |
359 test_result = trialRunner.runUntilFailure(suite) | |
360 else: | |
361 test_result = trialRunner.run(suite) | |
362 if config.tracer: | |
363 sys.settrace(None) | |
364 results = config.tracer.results() | |
365 results.write_results(show_missing=1, summary=False, | |
366 coverdir=config.coverdir) | |
367 sys.exit(not test_result.wasSuccessful()) | |
368 | |
OLD | NEW |