Merge lp:~james-w/testtools/discover into lp:~testtools-committers/testtools/trunk

Proposed by James Westby
Status: Merged
Approved by: Robert Collins
Approved revision: 67
Merged at revision: 67
Proposed branch: lp:~james-w/testtools/discover
Merge into: lp:~testtools-committers/testtools/trunk
Diff against target: 326 lines (+277/-9)
3 files modified
LICENSE (+19/-0)
README (+4/-0)
testtools/run.py (+254/-9)
To merge this branch: bzr merge lp:~james-w/testtools/discover
Reviewer Review Type Date Requested Status
testtools developers Pending
Review via email: mp+26994@code.launchpad.net

Description of the change

Support test discovery in testtools.run.

This is done by backporting TestProgram from 2.7 so that
we can use one interface to run the tests.

I had to make various tweaks where it assumed features that
won't be in older versions.

I dropped the parsing in the __name__ == '__main__' block, as
by my reading it swallows all options.

Thanks,

James

To post a comment you must log in.
lp:~james-w/testtools/discover updated
68. By James Westby

Move has_discover = True until we know we have set up everything we need.

69. By James Westby

Comment on the changes made to TestProgram.

Revision history for this message
Robert Collins (lifeless) wrote :

Approved-with-tweaks:
small bugfix in the conditional import (put the ok guard at the end)
document the changes made to the python2.7 code
list the (C) PSF code in README and COPYING

Revision history for this message
James Westby (james-w) wrote :

Tweaks made as requested.

Thanks,

James

lp:~james-w/testtools/discover updated
70. By James Westby

Add (C)/License information.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'LICENSE'
2--- LICENSE 2009-12-01 10:04:35 +0000
3+++ LICENSE 2010-06-10 22:16:25 +0000
4@@ -17,3 +17,22 @@
5 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
6 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
7 SOFTWARE.
8+
9+Some code in testtools/run.py taken from Python's unittest module:
10+Copyright (c) 1999-2003 Steve Purcell
11+Copyright (c) 2003-2010 Python Software Foundation
12+
13+This module is free software, and you may redistribute it and/or modify
14+it under the same terms as Python itself, so long as this copyright message
15+and disclaimer are retained in their original form.
16+
17+IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
18+SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
19+THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
20+DAMAGE.
21+
22+THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
23+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
24+PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
25+AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
26+SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
27
28=== modified file 'README'
29--- README 2009-12-01 10:38:07 +0000
30+++ README 2010-06-10 22:16:25 +0000
31@@ -14,6 +14,10 @@
32 This project is distributed under the MIT license and copyright is owned by
33 Jonathan M. Lange. See LICENSE for details.
34
35+Some code in testtools/run.py is taken from Python's unittest module, and
36+is copyright Steve Purcell and the Python Software Foundation, it is
37+distributed under the same license as Python, see LICENSE for details.
38+
39
40 Dependencies
41 ------------
42
43=== modified file 'testtools/run.py'
44--- testtools/run.py 2010-01-01 00:24:51 +0000
45+++ testtools/run.py 2010-06-10 22:16:25 +0000
46@@ -8,12 +8,29 @@
47 $ python -m testtools.run testtools.tests.test_suite
48 """
49
50+import os
51+import unittest
52+import types
53 import sys
54
55-from testtools.tests import test_suite
56 from testtools import TextTestResult
57
58
59+defaultTestLoader = unittest.defaultTestLoader
60+defaultTestLoaderCls = unittest.TestLoader
61+
62+if getattr(defaultTestLoader, 'discover', None) is None:
63+ try:
64+ import discover
65+ defaultTestLoader = discover.DiscoveringTestLoader()
66+ defaultTestLoaderCls = discover.DiscoveringTestLoader
67+ have_discover = True
68+ except ImportError:
69+ have_discover = False
70+else:
71+ have_discover = True
72+
73+
74 class TestToolsTestRunner(object):
75 """ A thunk object to support unittest.TestProgram."""
76
77@@ -27,13 +44,241 @@
78 result.stopTestRun()
79
80
81+####################
82+# Taken from python 2.7 and slightly modified for compatibility with
83+# older versions. Delete when 2.7 is the oldest supported version.
84+# Modifications:
85+# - Use have_discover to raise an error if the user tries to use
86+# discovery on an old version and doesn't have discover installed.
87+# - If --catch is given check that installHandler is available, as
88+# it won't be on old python versions.
89+
90+FAILFAST = " -f, --failfast Stop on first failure\n"
91+CATCHBREAK = " -c, --catch Catch control-C and display results\n"
92+BUFFEROUTPUT = " -b, --buffer Buffer stdout and stderr during test runs\n"
93+
94+USAGE_AS_MAIN = """\
95+Usage: %(progName)s [options] [tests]
96+
97+Options:
98+ -h, --help Show this message
99+ -v, --verbose Verbose output
100+ -q, --quiet Minimal output
101+%(failfast)s%(catchbreak)s%(buffer)s
102+Examples:
103+ %(progName)s test_module - run tests from test_module
104+ %(progName)s module.TestClass - run tests from module.TestClass
105+ %(progName)s module.Class.test_method - run specified test method
106+
107+[tests] can be a list of any number of test modules, classes and test
108+methods.
109+
110+Alternative Usage: %(progName)s discover [options]
111+
112+Options:
113+ -v, --verbose Verbose output
114+%(failfast)s%(catchbreak)s%(buffer)s -s directory Directory to start discovery ('.' default)
115+ -p pattern Pattern to match test files ('test*.py' default)
116+ -t directory Top level directory of project (default to
117+ start directory)
118+
119+For test discovery all test modules must be importable from the top
120+level directory of the project.
121+"""
122+
123+USAGE_FROM_MODULE = """\
124+Usage: %(progName)s [options] [test] [...]
125+
126+Options:
127+ -h, --help Show this message
128+ -v, --verbose Verbose output
129+ -q, --quiet Minimal output
130+%(failfast)s%(catchbreak)s%(buffer)s
131+Examples:
132+ %(progName)s - run default set of tests
133+ %(progName)s MyTestSuite - run suite 'MyTestSuite'
134+ %(progName)s MyTestCase.testSomething - run MyTestCase.testSomething
135+ %(progName)s MyTestCase - run all 'test*' test methods
136+ in MyTestCase
137+"""
138+
139+
140+class TestProgram(object):
141+ """A command-line program that runs a set of tests; this is primarily
142+ for making test modules conveniently executable.
143+ """
144+ USAGE = USAGE_FROM_MODULE
145+
146+ # defaults for testing
147+ failfast = catchbreak = buffer = progName = None
148+
149+ def __init__(self, module='__main__', defaultTest=None, argv=None,
150+ testRunner=None, testLoader=defaultTestLoader,
151+ exit=True, verbosity=1, failfast=None, catchbreak=None,
152+ buffer=None):
153+ if isinstance(module, basestring):
154+ self.module = __import__(module)
155+ for part in module.split('.')[1:]:
156+ self.module = getattr(self.module, part)
157+ else:
158+ self.module = module
159+ if argv is None:
160+ argv = sys.argv
161+
162+ self.exit = exit
163+ self.failfast = failfast
164+ self.catchbreak = catchbreak
165+ self.verbosity = verbosity
166+ self.buffer = buffer
167+ self.defaultTest = defaultTest
168+ self.testRunner = testRunner
169+ self.testLoader = testLoader
170+ self.progName = os.path.basename(argv[0])
171+ self.parseArgs(argv)
172+ self.runTests()
173+
174+ def usageExit(self, msg=None):
175+ if msg:
176+ print msg
177+ usage = {'progName': self.progName, 'catchbreak': '', 'failfast': '',
178+ 'buffer': ''}
179+ if self.failfast != False:
180+ usage['failfast'] = FAILFAST
181+ if self.catchbreak != False:
182+ usage['catchbreak'] = CATCHBREAK
183+ if self.buffer != False:
184+ usage['buffer'] = BUFFEROUTPUT
185+ print self.USAGE % usage
186+ sys.exit(2)
187+
188+ def parseArgs(self, argv):
189+ if len(argv) > 1 and argv[1].lower() == 'discover':
190+ self._do_discovery(argv[2:])
191+ return
192+
193+ import getopt
194+ long_opts = ['help', 'verbose', 'quiet', 'failfast', 'catch', 'buffer']
195+ try:
196+ options, args = getopt.getopt(argv[1:], 'hHvqfcb', long_opts)
197+ for opt, value in options:
198+ if opt in ('-h','-H','--help'):
199+ self.usageExit()
200+ if opt in ('-q','--quiet'):
201+ self.verbosity = 0
202+ if opt in ('-v','--verbose'):
203+ self.verbosity = 2
204+ if opt in ('-f','--failfast'):
205+ if self.failfast is None:
206+ self.failfast = True
207+ # Should this raise an exception if -f is not valid?
208+ if opt in ('-c','--catch'):
209+ if self.catchbreak is None:
210+ self.catchbreak = True
211+ # Should this raise an exception if -c is not valid?
212+ if opt in ('-b','--buffer'):
213+ if self.buffer is None:
214+ self.buffer = True
215+ # Should this raise an exception if -b is not valid?
216+ if len(args) == 0 and self.defaultTest is None:
217+ # createTests will load tests from self.module
218+ self.testNames = None
219+ elif len(args) > 0:
220+ self.testNames = args
221+ if __name__ == '__main__':
222+ # to support python -m unittest ...
223+ self.module = None
224+ else:
225+ self.testNames = (self.defaultTest,)
226+ self.createTests()
227+ except getopt.error, msg:
228+ self.usageExit(msg)
229+
230+ def createTests(self):
231+ if self.testNames is None:
232+ self.test = self.testLoader.loadTestsFromModule(self.module)
233+ else:
234+ self.test = self.testLoader.loadTestsFromNames(self.testNames,
235+ self.module)
236+
237+ def _do_discovery(self, argv, Loader=defaultTestLoaderCls):
238+ # handle command line args for test discovery
239+ if not have_discover:
240+ raise AssertionError("Unable to use discovery, must use python 2.7 "
241+ "or greater, or install the discover package.")
242+ self.progName = '%s discover' % self.progName
243+ import optparse
244+ parser = optparse.OptionParser()
245+ parser.prog = self.progName
246+ parser.add_option('-v', '--verbose', dest='verbose', default=False,
247+ help='Verbose output', action='store_true')
248+ if self.failfast != False:
249+ parser.add_option('-f', '--failfast', dest='failfast', default=False,
250+ help='Stop on first fail or error',
251+ action='store_true')
252+ if self.catchbreak != False:
253+ parser.add_option('-c', '--catch', dest='catchbreak', default=False,
254+ help='Catch ctrl-C and display results so far',
255+ action='store_true')
256+ if self.buffer != False:
257+ parser.add_option('-b', '--buffer', dest='buffer', default=False,
258+ help='Buffer stdout and stderr during tests',
259+ action='store_true')
260+ parser.add_option('-s', '--start-directory', dest='start', default='.',
261+ help="Directory to start discovery ('.' default)")
262+ parser.add_option('-p', '--pattern', dest='pattern', default='test*.py',
263+ help="Pattern to match tests ('test*.py' default)")
264+ parser.add_option('-t', '--top-level-directory', dest='top', default=None,
265+ help='Top level directory of project (defaults to start directory)')
266+
267+ options, args = parser.parse_args(argv)
268+ if len(args) > 3:
269+ self.usageExit()
270+
271+ for name, value in zip(('start', 'pattern', 'top'), args):
272+ setattr(options, name, value)
273+
274+ # only set options from the parsing here
275+ # if they weren't set explicitly in the constructor
276+ if self.failfast is None:
277+ self.failfast = options.failfast
278+ if self.catchbreak is None:
279+ self.catchbreak = options.catchbreak
280+ if self.buffer is None:
281+ self.buffer = options.buffer
282+
283+ if options.verbose:
284+ self.verbosity = 2
285+
286+ start_dir = options.start
287+ pattern = options.pattern
288+ top_level_dir = options.top
289+
290+ loader = Loader()
291+ self.test = loader.discover(start_dir, pattern, top_level_dir)
292+
293+ def runTests(self):
294+ if (self.catchbreak
295+ and getattr(unittest, 'installHandler', None) is not None):
296+ unittest.installHandler()
297+ if self.testRunner is None:
298+ self.testRunner = runner.TextTestRunner
299+ if isinstance(self.testRunner, (type, types.ClassType)):
300+ try:
301+ testRunner = self.testRunner(verbosity=self.verbosity,
302+ failfast=self.failfast,
303+ buffer=self.buffer)
304+ except TypeError:
305+ # didn't accept the verbosity, buffer or failfast arguments
306+ testRunner = self.testRunner()
307+ else:
308+ # it is assumed to be a TestRunner instance
309+ testRunner = self.testRunner
310+ self.result = testRunner.run(self.test)
311+ if self.exit:
312+ sys.exit(not self.result.wasSuccessful())
313+################
314+
315+
316 if __name__ == '__main__':
317- import optparse
318- from unittest import TestProgram
319- parser = optparse.OptionParser(__doc__)
320- args = parser.parse_args()[1]
321- if not args:
322- parser.error("No testspecs given.")
323 runner = TestToolsTestRunner()
324- program = TestProgram(module=None, argv=[sys.argv[0]] + args,
325- testRunner=runner)
326+ program = TestProgram(argv=sys.argv, testRunner=runner)

Subscribers

People subscribed via source and target branches