1 # -*- test-case-name: twisted.trial.test.test_script -*-
3 # Copyright (c) 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, versions, deprecate
12 from twisted.python.filepath import FilePath
13 from twisted import plugin
14 from twisted.python.util import spewer
15 from twisted.python.compat import set
16 from twisted.trial import runner, itrial, reporter
19 # Yea, this is stupid. Leave it for for command-line compatibility for a
31 def _parseLocalVariables(line):
33 Accepts a single line in Emacs local variable declaration format and
34 returns a dict of all the variables {name: value}.
35 Raises ValueError if 'line' is in the wrong format.
37 See http://www.gnu.org/software/emacs/manual/html_node/File-Variables.html
40 start = line.find(paren) + len(paren)
41 end = line.rfind(paren)
42 if start == -1 or end == -1:
43 raise ValueError("%r not a valid local variable declaration" % (line,))
44 items = line[start:end].split(';')
47 if len(item.strip()) == 0:
49 split = item.split(':')
51 raise ValueError("%r contains invalid declaration %r"
53 localVars[split[0].strip()] = split[1].strip()
57 def loadLocalVariables(filename):
59 Accepts a filename and attempts to load the Emacs variable declarations
60 from that file, simulating what Emacs does.
62 See http://www.gnu.org/software/emacs/manual/html_node/File-Variables.html
64 f = file(filename, "r")
65 lines = [f.readline(), f.readline()]
69 return _parseLocalVariables(line)
75 def getTestModules(filename):
76 testCaseVar = loadLocalVariables(filename).get('test-case-name', None)
77 if testCaseVar is None:
79 return testCaseVar.split(',')
82 def isTestFile(filename):
84 Returns true if 'filename' looks like a file containing unit tests.
85 False otherwise. Doesn't care whether filename exists.
87 basename = os.path.basename(filename)
88 return (basename.startswith('test_')
89 and os.path.splitext(basename)[1] == ('.py'))
92 def _reporterAction():
93 return usage.CompleteList([p.longOpt for p in
94 plugin.getPlugins(itrial.IReporter)])
96 class Options(usage.Options, app.ReactorSelectionMixin):
97 synopsis = """%s [options] [[file|package|module|TestCase|testmethod]...]
98 """ % (os.path.basename(sys.argv[0]),)
100 longdesc = ("trial loads and executes a suite of unit tests, obtained "
101 "from modules, packages and files listed on the command line.")
103 optFlags = [["help", "h"],
104 ["rterrors", "e", "realtime errors, print out tracebacks as "
105 "soon as they occur"],
106 ["debug", "b", "Run tests in the Python debugger. Will load "
107 "'.pdbrc' from current directory if it exists."],
108 ["debug-stacktraces", "B", "Report Deferred creation and "
109 "callback stack traces"],
110 ["nopm", None, "don't automatically jump into debugger for "
111 "postmorteming of exceptions"],
112 ["dry-run", 'n', "do everything but run the tests"],
113 ["force-gc", None, "Have Trial run gc.collect() before and "
114 "after each test case."],
115 ["profile", None, "Run tests under the Python profiler"],
116 ["unclean-warnings", None,
117 "Turn dirty reactor errors into warnings"],
118 ["until-failure", "u", "Repeat test until it fails"],
119 ["no-recurse", "N", "Don't recurse into packages"],
120 ['help-reporters', None,
121 "Help on available output plugins (reporters)"]
125 ["logfile", "l", "test.log", "log file name"],
126 ["random", "z", None,
127 "Run tests in random order using the specified seed"],
128 ['temp-directory', None, '_trial_temp',
129 'Path to use as working directory for tests.'],
130 ['reporter', None, 'verbose',
131 'The reporter to use for this test run. See --help-reporters for '
134 compData = usage.Completions(
135 optActions={"tbformat": usage.CompleteList(["plain", "emacs", "cgitb"]),
136 "reporter": _reporterAction,
137 "logfile": usage.CompleteFiles(descr="log file name"),
138 "random": usage.Completer(descr="random seed")},
139 extraActions=[usage.CompleteFiles(
140 "*.py", descr="file | module | package | TestCase | testMethod",
144 fallbackReporter = reporter.TreeReporter
149 self['tests'] = set()
150 usage.Options.__init__(self)
155 Return a L{FilePath} representing the directory into which coverage
156 results should be written.
158 coverdir = 'coverage'
159 result = FilePath(self['temp-directory']).child(coverdir)
160 print "Setting coverage directory to %s." % (result.path,)
164 def opt_coverage(self):
166 Generate coverage information in the I{coverage} file in the
167 directory specified by the I{trial-temp} option.
170 self.tracer = trace.Trace(count=1, trace=0)
171 sys.settrace(self.tracer.globaltrace)
174 def opt_testmodule(self, filename):
176 Filename to grep for test cases (-*- test-case-name)
178 # If the filename passed to this parameter looks like a test module
179 # we just add that to the test suite.
181 # If not, we inspect it for an Emacs buffer local variable called
182 # 'test-case-name'. If that variable is declared, we try to add its
183 # value to the test suite as a module.
185 # This parameter allows automated processes (like Buildbot) to pass
186 # a list of files to Trial with the general expectation of "these files,
187 # whatever they are, will get tested"
188 if not os.path.isfile(filename):
189 sys.stderr.write("File %r doesn't exist\n" % (filename,))
191 filename = os.path.abspath(filename)
192 if isTestFile(filename):
193 self['tests'].add(filename)
195 self['tests'].update(getTestModules(filename))
200 Print an insanely verbose log of everything that happens. Useful
201 when debugging freezes or locks in complex code.
206 def opt_help_reporters(self):
207 synopsis = ("Trial's output can be customized using plugins called "
208 "Reporters. You can\nselect any of the following "
209 "reporters using --reporter=<foo>\n")
211 for p in plugin.getPlugins(itrial.IReporter):
212 print ' ', p.longOpt, '\t', p.description
217 def opt_disablegc(self):
219 Disable the garbage collector
224 def opt_tbformat(self, opt):
226 Specify the format to display tracebacks with. Valid formats are
227 'plain', 'emacs', and 'cgitb' which uses the nicely verbose stdlib
231 self['tbformat'] = TBFORMAT_MAP[opt]
233 raise usage.UsageError(
234 "tbformat must be 'plain', 'emacs', or 'cgitb'.")
237 def opt_extra(self, arg):
239 Add an extra argument. (This is a hack necessary for interfacing with
240 emacs's `gud'.) NOTE: This option is deprecated as of Twisted 11.0
242 warnings.warn(deprecate.getDeprecationWarningString(Options.opt_extra,
243 versions.Version('Twisted', 11, 0, 0)),
244 category=DeprecationWarning, stacklevel=2)
246 if self.extra is None:
248 self.extra.append(arg)
252 def opt_recursionlimit(self, arg):
254 see sys.setrecursionlimit()
257 sys.setrecursionlimit(int(arg))
258 except (TypeError, ValueError):
259 raise usage.UsageError(
260 "argument to recursionlimit must be an integer")
263 def opt_random(self, option):
265 self['random'] = long(option)
267 raise usage.UsageError(
268 "Argument to --random must be a positive integer")
270 if self['random'] < 0:
271 raise usage.UsageError(
272 "Argument to --random must be a positive integer")
273 elif self['random'] == 0:
274 self['random'] = long(time.time() * 100)
277 def opt_without_module(self, option):
279 Fake the lack of the specified modules, separated with commas.
281 for module in option.split(","):
282 if module in sys.modules:
283 warnings.warn("Module '%s' already imported, "
284 "disabling anyway." % (module,),
285 category=RuntimeWarning)
286 sys.modules[module] = None
289 def parseArgs(self, *args):
290 self['tests'].update(args)
291 if self.extra is not None:
292 self['tests'].update(self.extra)
295 def _loadReporterByName(self, name):
296 for p in plugin.getPlugins(itrial.IReporter):
297 qual = "%s.%s" % (p.module, p.klass)
298 if p.longOpt == name:
299 return reflect.namedAny(qual)
300 raise usage.UsageError("Only pass names of Reporter plugins to "
301 "--reporter. See --help-reporters for "
305 def postOptions(self):
306 # Only load reporters now, as opposed to any earlier, to avoid letting
307 # application-defined plugins muck up reactor selecting by importing
308 # t.i.reactor and causing the default to be installed.
309 self['reporter'] = self._loadReporterByName(self['reporter'])
311 if 'tbformat' not in self:
312 self['tbformat'] = 'default'
314 if not self['debug']:
315 raise usage.UsageError("you must specify --debug when using "
317 failure.DO_POST_MORTEM = False
321 def _initialDebugSetup(config):
322 # do this part of debug setup first for easy debugging of import failures
324 failure.startDebugMode()
325 if config['debug'] or config['debug-stacktraces']:
326 defer.setDebugging(True)
330 def _getSuite(config):
331 loader = _getLoader(config)
332 recurse = not config['no-recurse']
333 return loader.loadByNames(config['tests'], recurse)
337 def _getLoader(config):
338 loader = runner.TestLoader()
340 randomer = random.Random()
341 randomer.seed(config['random'])
342 loader.sorter = lambda x : randomer.random()
343 print 'Running tests shuffled with seed %d\n' % config['random']
344 if not config['until-failure']:
345 loader.suiteFactory = runner.DestructiveTestSuite
350 def _makeRunner(config):
353 mode = runner.TrialRunner.DEBUG
354 if config['dry-run']:
355 mode = runner.TrialRunner.DRY_RUN
356 return runner.TrialRunner(config['reporter'],
358 profile=config['profile'],
359 logfile=config['logfile'],
360 tracebackFormat=config['tbformat'],
361 realTimeErrors=config['rterrors'],
362 uncleanWarnings=config['unclean-warnings'],
363 workingDirectory=config['temp-directory'],
364 forceGarbageCollection=config['force-gc'])
369 if len(sys.argv) == 1:
370 sys.argv.append("--help")
373 config.parseOptions()
374 except usage.error, ue:
375 raise SystemExit, "%s: %s" % (sys.argv[0], ue)
376 _initialDebugSetup(config)
377 trialRunner = _makeRunner(config)
378 suite = _getSuite(config)
379 if config['until-failure']:
380 test_result = trialRunner.runUntilFailure(suite)
382 test_result = trialRunner.run(suite)
385 results = config.tracer.results()
386 results.write_results(show_missing=1, summary=False,
387 coverdir=config.coverdir().path)
388 sys.exit(not test_result.wasSuccessful())