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