1
# -*- test-case-name: twisted.trial.test.test_script -*-
3
# Copyright (c) 2001-2007 Twisted Matrix Laboratories.
4
# See LICENSE for details.
7
import sys, os, random, gc, time, warnings
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
18
# Yea, this is stupid. Leave it for for command-line compatibility for a
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.
35
See http://www.gnu.org/software/emacs/manual/html_node/File-Variables.html
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(';')
45
if len(item.strip()) == 0:
47
split = item.split(':')
49
raise ValueError("%r contains invalid declaration %r"
51
localVars[split[0].strip()] = split[1].strip()
55
def loadLocalVariables(filename):
56
"""Accepts a filename and attempts to load the Emacs variable declarations
57
from that file, simulating what Emacs does.
59
See http://www.gnu.org/software/emacs/manual/html_node/File-Variables.html
61
f = file(filename, "r")
62
lines = [f.readline(), f.readline()]
66
return _parseLocalVariables(line)
72
def getTestModules(filename):
73
testCaseVar = loadLocalVariables(filename).get('test-case-name', None)
74
if testCaseVar is None:
76
return testCaseVar.split(',')
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.
83
basename = os.path.basename(filename)
84
return (basename.startswith('test_')
85
and os.path.splitext(basename)[1] == ('.py'))
88
def _zshReporterAction():
89
return "(%s)" % (" ".join([p.longOpt for p in plugin.getPlugins(itrial.IReporter)]),)
91
class Options(usage.Options, app.ReactorSelectionMixin):
92
synopsis = """%s [options] [[file|package|module|TestCase|testmethod]...]
93
""" % (os.path.basename(sys.argv[0]),)
94
longdesc = ("trial loads and executes a suite of unit tests, obtained "
95
"from modules, packages and files listed on the command line.")
97
optFlags = [["help", "h"],
98
["rterrors", "e", "realtime errors, print out tracebacks as "
99
"soon as they occur"],
100
["debug", "b", "Run tests in the Python debugger. Will load "
101
"'.pdbrc' from current directory if it exists."],
102
["debug-stacktraces", "B", "Report Deferred creation and "
103
"callback stack traces"],
104
["nopm", None, "don't automatically jump into debugger for "
105
"postmorteming of exceptions"],
106
["dry-run", 'n', "do everything but run the tests"],
107
["force-gc", None, "Have Trial run gc.collect() before and "
108
"after each test case."],
109
["profile", None, "Run tests under the Python profiler"],
110
["unclean-warnings", None,
111
"Turn dirty reactor errors into warnings"],
112
["until-failure", "u", "Repeat test until it fails"],
113
["no-recurse", "N", "Don't recurse into packages"],
114
['help-reporters', None,
115
"Help on available output plugins (reporters)"]
119
["logfile", "l", "test.log", "log file name"],
120
["random", "z", None,
121
"Run tests in random order using the specified seed"],
122
['temp-directory', None, '_trial_temp',
123
'Path to use as working directory for tests.'],
124
['reporter', None, 'verbose',
125
'The reporter to use for this test run. See --help-reporters for '
128
zsh_actions = {"tbformat":"(plain emacs cgitb)",
129
"reporter":_zshReporterAction}
130
zsh_actionDescr = {"logfile":"log file name",
131
"random":"random seed"}
132
zsh_extras = ["*:file|module|package|TestCase|testMethod:_files -g '*.py'"]
134
fallbackReporter = reporter.TreeReporter
139
self['tests'] = set()
140
usage.Options.__init__(self)
142
def opt_coverage(self):
144
Generate coverage information in the _trial_temp/coverage. Requires
147
coverdir = 'coverage'
148
print "Setting coverage directory to %s." % (coverdir,)
151
# begin monkey patch ---------------------------
152
# Before Python 2.4, this function asserted that 'filename' had
153
# to end with '.py' This is wrong for at least two reasons:
154
# 1. We might be wanting to find executable line nos in a script
155
# 2. The implementation should use os.splitext
156
# This monkey patch is the same function as in the stdlib (v2.3)
157
# but with the assertion removed.
158
def find_executable_linenos(filename):
159
"""Return dict where keys are line numbers in the line number
162
#assert filename.endswith('.py') # YOU BASTARDS
164
prog = open(filename).read()
165
prog = '\n'.join(prog.splitlines()) + '\n'
167
sys.stderr.write("Not printing coverage data for %r: %s\n"
171
code = compile(prog, filename, "exec")
172
strs = trace.find_strings(filename)
173
return trace.find_lines(code, strs)
175
trace.find_executable_linenos = find_executable_linenos
176
# end monkey patch ------------------------------
178
self.coverdir = os.path.abspath(os.path.join(self['temp-directory'], coverdir))
179
self.tracer = trace.Trace(count=1, trace=0)
180
sys.settrace(self.tracer.globaltrace)
182
def opt_testmodule(self, filename):
183
"Filename to grep for test cases (-*- test-case-name)"
184
# If the filename passed to this parameter looks like a test module
185
# we just add that to the test suite.
187
# If not, we inspect it for an Emacs buffer local variable called
188
# 'test-case-name'. If that variable is declared, we try to add its
189
# value to the test suite as a module.
191
# This parameter allows automated processes (like Buildbot) to pass
192
# a list of files to Trial with the general expectation of "these files,
193
# whatever they are, will get tested"
194
if not os.path.isfile(filename):
195
sys.stderr.write("File %r doesn't exist\n" % (filename,))
197
filename = os.path.abspath(filename)
198
if isTestFile(filename):
199
self['tests'].add(filename)
201
self['tests'].update(getTestModules(filename))
204
"""Print an insanely verbose log of everything that happens. Useful
205
when debugging freezes or locks in complex code."""
209
def opt_help_reporters(self):
210
synopsis = ("Trial's output can be customized using plugins called "
211
"Reporters. You can\nselect any of the following "
212
"reporters using --reporter=<foo>\n")
214
for p in plugin.getPlugins(itrial.IReporter):
215
print ' ', p.longOpt, '\t', p.description
219
def opt_disablegc(self):
220
"""Disable the garbage collector"""
223
def opt_tbformat(self, opt):
224
"""Specify the format to display tracebacks with. Valid formats are
225
'plain', 'emacs', and 'cgitb' which uses the nicely verbose stdlib
226
cgitb.text function"""
228
self['tbformat'] = TBFORMAT_MAP[opt]
230
raise usage.UsageError(
231
"tbformat must be 'plain', 'emacs', or 'cgitb'.")
233
def opt_extra(self, arg):
235
Add an extra argument. (This is a hack necessary for interfacing with
238
if self.extra is None:
240
self.extra.append(arg)
243
def opt_recursionlimit(self, arg):
244
"""see sys.setrecursionlimit()"""
246
sys.setrecursionlimit(int(arg))
247
except (TypeError, ValueError):
248
raise usage.UsageError(
249
"argument to recursionlimit must be an integer")
251
def opt_random(self, option):
253
self['random'] = long(option)
255
raise usage.UsageError(
256
"Argument to --random must be a positive integer")
258
if self['random'] < 0:
259
raise usage.UsageError(
260
"Argument to --random must be a positive integer")
261
elif self['random'] == 0:
262
self['random'] = long(time.time() * 100)
264
def opt_without_module(self, option):
266
Fake the lack of the specified modules, separated with commas.
268
for module in option.split(","):
269
if module in sys.modules:
270
warnings.warn("Module '%s' already imported, "
271
"disabling anyway." % (module,),
272
category=RuntimeWarning)
273
sys.modules[module] = None
275
def parseArgs(self, *args):
276
self['tests'].update(args)
277
if self.extra is not None:
278
self['tests'].update(self.extra)
280
def _loadReporterByName(self, name):
281
for p in plugin.getPlugins(itrial.IReporter):
282
qual = "%s.%s" % (p.module, p.klass)
283
if p.longOpt == name:
284
return reflect.namedAny(qual)
285
raise usage.UsageError("Only pass names of Reporter plugins to "
286
"--reporter. See --help-reporters for "
290
def postOptions(self):
292
# Only load reporters now, as opposed to any earlier, to avoid letting
293
# application-defined plugins muck up reactor selecting by importing
294
# t.i.reactor and causing the default to be installed.
295
self['reporter'] = self._loadReporterByName(self['reporter'])
297
if 'tbformat' not in self:
298
self['tbformat'] = 'default'
300
if not self['debug']:
301
raise usage.UsageError("you must specify --debug when using "
303
failure.DO_POST_MORTEM = False
306
def _initialDebugSetup(config):
307
# do this part of debug setup first for easy debugging of import failures
309
failure.startDebugMode()
310
if config['debug'] or config['debug-stacktraces']:
311
defer.setDebugging(True)
314
def _getSuite(config):
315
loader = _getLoader(config)
316
recurse = not config['no-recurse']
317
return loader.loadByNames(config['tests'], recurse)
320
def _getLoader(config):
321
loader = runner.TestLoader()
323
randomer = random.Random()
324
randomer.seed(config['random'])
325
loader.sorter = lambda x : randomer.random()
326
print 'Running tests shuffled with seed %d\n' % config['random']
327
if not config['until-failure']:
328
loader.suiteFactory = runner.DestructiveTestSuite
332
def _makeRunner(config):
335
mode = runner.TrialRunner.DEBUG
336
if config['dry-run']:
337
mode = runner.TrialRunner.DRY_RUN
338
return runner.TrialRunner(config['reporter'],
340
profile=config['profile'],
341
logfile=config['logfile'],
342
tracebackFormat=config['tbformat'],
343
realTimeErrors=config['rterrors'],
344
uncleanWarnings=config['unclean-warnings'],
345
workingDirectory=config['temp-directory'],
346
forceGarbageCollection=config['force-gc'])
350
if len(sys.argv) == 1:
351
sys.argv.append("--help")
354
config.parseOptions()
355
except usage.error, ue:
356
raise SystemExit, "%s: %s" % (sys.argv[0], ue)
357
_initialDebugSetup(config)
358
trialRunner = _makeRunner(config)
359
suite = _getSuite(config)
360
if config['until-failure']:
361
test_result = trialRunner.runUntilFailure(suite)
363
test_result = trialRunner.run(suite)
366
results = config.tracer.results()
367
results.write_results(show_missing=1, summary=False,
368
coverdir=config.coverdir)
369
sys.exit(not test_result.wasSuccessful())