Imported Upstream version 12.1.0
[contrib/python-twisted.git] / twisted / scripts / trial.py
1 # -*- test-case-name: twisted.trial.test.test_script -*-
2
3 # Copyright (c) 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, 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
17
18
19 # Yea, this is stupid.  Leave it for for command-line compatibility for a
20 # while, though.
21 TBFORMAT_MAP = {
22     'plain': 'default',
23     'default': 'default',
24     'emacs': 'brief',
25     'brief': 'brief',
26     'cgitb': 'verbose',
27     'verbose': 'verbose'
28     }
29
30
31 def _parseLocalVariables(line):
32     """
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.
36
37     See http://www.gnu.org/software/emacs/manual/html_node/File-Variables.html
38     """
39     paren = '-*-'
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(';')
45     localVars = {}
46     for item in items:
47         if len(item.strip()) == 0:
48             continue
49         split = item.split(':')
50         if len(split) != 2:
51             raise ValueError("%r contains invalid declaration %r"
52                              % (line, item))
53         localVars[split[0].strip()] = split[1].strip()
54     return localVars
55
56
57 def loadLocalVariables(filename):
58     """
59     Accepts a filename and attempts to load the Emacs variable declarations
60     from that file, simulating what Emacs does.
61
62     See http://www.gnu.org/software/emacs/manual/html_node/File-Variables.html
63     """
64     f = file(filename, "r")
65     lines = [f.readline(), f.readline()]
66     f.close()
67     for line in lines:
68         try:
69             return _parseLocalVariables(line)
70         except ValueError:
71             pass
72     return {}
73
74
75 def getTestModules(filename):
76     testCaseVar = loadLocalVariables(filename).get('test-case-name', None)
77     if testCaseVar is None:
78         return []
79     return testCaseVar.split(',')
80
81
82 def isTestFile(filename):
83     """
84     Returns true if 'filename' looks like a file containing unit tests.
85     False otherwise.  Doesn't care whether filename exists.
86     """
87     basename = os.path.basename(filename)
88     return (basename.startswith('test_')
89             and os.path.splitext(basename)[1] == ('.py'))
90
91
92 def _reporterAction():
93     return usage.CompleteList([p.longOpt for p in
94                                plugin.getPlugins(itrial.IReporter)])
95
96 class Options(usage.Options, app.ReactorSelectionMixin):
97     synopsis = """%s [options] [[file|package|module|TestCase|testmethod]...]
98     """ % (os.path.basename(sys.argv[0]),)
99
100     longdesc = ("trial loads and executes a suite of unit tests, obtained "
101                 "from modules, packages and files listed on the command line.")
102
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)"]
122                 ]
123
124     optParameters = [
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 '
132          'more info.']]
133
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",
141                 repeat=True)],
142         )
143
144     fallbackReporter = reporter.TreeReporter
145     extra = None
146     tracer = None
147
148     def __init__(self):
149         self['tests'] = set()
150         usage.Options.__init__(self)
151
152
153     def coverdir(self):
154         """
155         Return a L{FilePath} representing the directory into which coverage
156         results should be written.
157         """
158         coverdir = 'coverage'
159         result = FilePath(self['temp-directory']).child(coverdir)
160         print "Setting coverage directory to %s." % (result.path,)
161         return result
162
163
164     def opt_coverage(self):
165         """
166         Generate coverage information in the I{coverage} file in the
167         directory specified by the I{trial-temp} option.
168         """
169         import trace
170         self.tracer = trace.Trace(count=1, trace=0)
171         sys.settrace(self.tracer.globaltrace)
172
173
174     def opt_testmodule(self, filename):
175         """
176         Filename to grep for test cases (-*- test-case-name)
177         """
178         # If the filename passed to this parameter looks like a test module
179         # we just add that to the test suite.
180         #
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.
184         #
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,))
190             return
191         filename = os.path.abspath(filename)
192         if isTestFile(filename):
193             self['tests'].add(filename)
194         else:
195             self['tests'].update(getTestModules(filename))
196
197
198     def opt_spew(self):
199         """
200         Print an insanely verbose log of everything that happens.  Useful
201         when debugging freezes or locks in complex code.
202         """
203         sys.settrace(spewer)
204
205
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")
210         print synopsis
211         for p in plugin.getPlugins(itrial.IReporter):
212             print '   ', p.longOpt, '\t', p.description
213         print
214         sys.exit(0)
215
216
217     def opt_disablegc(self):
218         """
219         Disable the garbage collector
220         """
221         gc.disable()
222
223
224     def opt_tbformat(self, opt):
225         """
226         Specify the format to display tracebacks with. Valid formats are
227         'plain', 'emacs', and 'cgitb' which uses the nicely verbose stdlib
228         cgitb.text function
229         """
230         try:
231             self['tbformat'] = TBFORMAT_MAP[opt]
232         except KeyError:
233             raise usage.UsageError(
234                 "tbformat must be 'plain', 'emacs', or 'cgitb'.")
235
236
237     def opt_extra(self, arg):
238         """
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
241         """
242         warnings.warn(deprecate.getDeprecationWarningString(Options.opt_extra,
243                                                             versions.Version('Twisted', 11, 0, 0)),
244                       category=DeprecationWarning, stacklevel=2)
245
246         if self.extra is None:
247             self.extra = []
248         self.extra.append(arg)
249     opt_x = opt_extra
250
251
252     def opt_recursionlimit(self, arg):
253         """
254         see sys.setrecursionlimit()
255         """
256         try:
257             sys.setrecursionlimit(int(arg))
258         except (TypeError, ValueError):
259             raise usage.UsageError(
260                 "argument to recursionlimit must be an integer")
261
262
263     def opt_random(self, option):
264         try:
265             self['random'] = long(option)
266         except ValueError:
267             raise usage.UsageError(
268                 "Argument to --random must be a positive integer")
269         else:
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)
275
276
277     def opt_without_module(self, option):
278         """
279         Fake the lack of the specified modules, separated with commas.
280         """
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
287
288
289     def parseArgs(self, *args):
290         self['tests'].update(args)
291         if self.extra is not None:
292             self['tests'].update(self.extra)
293
294
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 "
302                                "more info.")
303
304
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'])
310
311         if 'tbformat' not in self:
312             self['tbformat'] = 'default'
313         if self['nopm']:
314             if not self['debug']:
315                 raise usage.UsageError("you must specify --debug when using "
316                                        "--nopm ")
317             failure.DO_POST_MORTEM = False
318
319
320
321 def _initialDebugSetup(config):
322     # do this part of debug setup first for easy debugging of import failures
323     if config['debug']:
324         failure.startDebugMode()
325     if config['debug'] or config['debug-stacktraces']:
326         defer.setDebugging(True)
327
328
329
330 def _getSuite(config):
331     loader = _getLoader(config)
332     recurse = not config['no-recurse']
333     return loader.loadByNames(config['tests'], recurse)
334
335
336
337 def _getLoader(config):
338     loader = runner.TestLoader()
339     if config['random']:
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
346     return loader
347
348
349
350 def _makeRunner(config):
351     mode = None
352     if config['debug']:
353         mode = runner.TrialRunner.DEBUG
354     if config['dry-run']:
355         mode = runner.TrialRunner.DRY_RUN
356     return runner.TrialRunner(config['reporter'],
357                               mode=mode,
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'])
365
366
367
368 def run():
369     if len(sys.argv) == 1:
370         sys.argv.append("--help")
371     config = Options()
372     try:
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)
381     else:
382         test_result = trialRunner.run(suite)
383     if config.tracer:
384         sys.settrace(None)
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())
389