Merge lp:~dobey/ubuntuone-dev-tools/runner-refactor into lp:ubuntuone-dev-tools
- runner-refactor
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Alejandro J. Cura |
Approved revision: | 82 |
Merged at revision: | 77 |
Proposed branch: | lp:~dobey/ubuntuone-dev-tools/runner-refactor |
Merge into: | lp:ubuntuone-dev-tools |
Diff against target: |
993 lines (+619/-305) 8 files modified
bin/u1trial (+2/-303) run-tests (+1/-1) setup.py (+1/-0) ubuntuone/devtools/errors.py (+35/-0) ubuntuone/devtools/runners/__init__.py (+286/-0) ubuntuone/devtools/runners/txrunner.py (+126/-0) ubuntuone/devtools/testcases/dbus.py (+1/-1) ubuntuone/devtools/utils.py (+167/-0) |
To merge this branch: | bzr merge lp:~dobey/ubuntuone-dev-tools/runner-refactor |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Alejandro J. Cura (community) | Approve | ||
Roberto Alsina (community) | Approve | ||
Review via email: mp+117122@code.launchpad.net |
Commit message
Refactor u1trial to allow multiple different test running back-ends
Implement Option handling API that is compatible with twisted trial
Remove logic from u1trial script
Fix a typo in the dbus test case
Ignore .pc directory for pep8 in run-tests
Description of the change
Roberto Alsina (ralsina) : | # |
dobey (dobey) wrote : | # |
Have merged the flag/params parsers into a single function which the other two call now.
As for testing, I haven't filed a bug yet, as I had just realized something about the testing and wanted to discuss first. We can probably add more tests, but as this code is required to work for u1trial to run, and we're running the dev-tools tests with the u1trial which is in the branch itself, the code has at least 90% coverage, if not 100%, simply by running the tests already. I suppose we could make it >100% by adding a bunch of fake objects and options lists, and ensuring somehow that the parsed output is what we'd expect, but also not sure how useful that is right now.
dobey (dobey) wrote : | # |
Bug about needing more tests is now filed as bug #1032336.
Alejandro J. Cura (alecu) wrote : | # |
Refactored code looks good.|
Preview Diff
1 | === modified file 'bin/u1trial' |
2 | --- bin/u1trial 2012-06-05 21:33:33 +0000 |
3 | +++ bin/u1trial 2012-08-02 15:49:22 +0000 |
4 | @@ -26,315 +26,14 @@ |
5 | # do not wish to do so, delete this exception statement from your |
6 | # version. If you delete this exception statement from all source |
7 | # files in the program, then also delete it here. |
8 | -"""Test runner that uses a private dbus session and glib main loop.""" |
9 | +"""Test runner which works with special services and main loops.""" |
10 | |
11 | -import coverage |
12 | -import gc |
13 | -import inspect |
14 | import os |
15 | -import re |
16 | import sys |
17 | -import unittest |
18 | - |
19 | -from twisted.python.usage import UsageError |
20 | -from twisted.scripts import trial |
21 | -from twisted.trial.runner import TrialRunner |
22 | |
23 | sys.path.insert(0, os.path.abspath(".")) |
24 | |
25 | -from ubuntuone.devtools.testing.txcheck import TXCheckSuite |
26 | - |
27 | - |
28 | -def _is_in_ignored_path(testcase, paths): |
29 | - """Return if the testcase is in one of the ignored paths.""" |
30 | - for ignored_path in paths: |
31 | - if testcase.startswith(ignored_path): |
32 | - return True |
33 | - return False |
34 | - |
35 | - |
36 | -class TestRunner(TrialRunner): |
37 | - """The test runner implementation.""" |
38 | - |
39 | - def __init__(self, config=None): |
40 | - # set $HOME to the _trial_temp dir, to avoid breaking user files |
41 | - trial_temp_dir = os.environ.get('TRIAL_TEMP_DIR', os.getcwd()) |
42 | - homedir = os.path.join(trial_temp_dir, config['temp-directory']) |
43 | - os.environ['HOME'] = homedir |
44 | - |
45 | - # setup $XDG_*_HOME variables and create the directories |
46 | - xdg_cache = os.path.join(homedir, 'xdg_cache') |
47 | - xdg_config = os.path.join(homedir, 'xdg_config') |
48 | - xdg_data = os.path.join(homedir, 'xdg_data') |
49 | - os.environ['XDG_CACHE_HOME'] = xdg_cache |
50 | - os.environ['XDG_CONFIG_HOME'] = xdg_config |
51 | - os.environ['XDG_DATA_HOME'] = xdg_data |
52 | - |
53 | - if not os.path.exists(xdg_cache): |
54 | - os.makedirs(xdg_cache) |
55 | - if not os.path.exists(xdg_config): |
56 | - os.makedirs(xdg_config) |
57 | - if not os.path.exists(xdg_data): |
58 | - os.makedirs(xdg_data) |
59 | - |
60 | - # setup the ROOTDIR env var |
61 | - os.environ['ROOTDIR'] = os.getcwd() |
62 | - |
63 | - # Need an attribute for tempdir so we can use it later |
64 | - self.tempdir = homedir |
65 | - working_dir = os.path.join(self.tempdir, 'trial') |
66 | - |
67 | - # Handle running trial in debug or dry-run mode |
68 | - mode = None |
69 | - if config['debug']: |
70 | - mode = TrialRunner.DEBUG |
71 | - if config['dry-run']: |
72 | - mode = TrialRunner.DRY_RUN |
73 | - |
74 | - # Hook up to the parent test runner |
75 | - super(TestRunner, self).__init__( |
76 | - reporterFactory=config['reporter'], |
77 | - mode=mode, |
78 | - profile=config['profile'], |
79 | - logfile=config['logfile'], |
80 | - tracebackFormat=config['tbformat'], |
81 | - realTimeErrors=config['rterrors'], |
82 | - uncleanWarnings=config['unclean-warnings'], |
83 | - workingDirectory=working_dir, |
84 | - forceGarbageCollection=config['force-gc']) |
85 | - |
86 | - self.required_services = [] |
87 | - self.source_files = [] |
88 | - |
89 | - def _load_unittest(self, relpath): |
90 | - """Load unit tests from a Python module with the given 'relpath'.""" |
91 | - assert relpath.endswith(".py"), ( |
92 | - "%s does not appear to be a Python module" % relpath) |
93 | - if not os.path.basename(relpath).startswith('test_'): |
94 | - return |
95 | - modpath = relpath.replace(os.path.sep, ".")[:-3] |
96 | - module = __import__(modpath, None, None, [""]) |
97 | - |
98 | - # If the module specifies required_services, make sure we get them |
99 | - members = [x[1] for x in inspect.getmembers(module, inspect.isclass)] |
100 | - for member_type in members: |
101 | - if hasattr(member_type, 'required_services'): |
102 | - member = member_type() |
103 | - for service in member.required_services(): |
104 | - if service not in self.required_services: |
105 | - self.required_services.append(service) |
106 | - del member |
107 | - gc.collect() |
108 | - |
109 | - # If the module has a 'suite' or 'test_suite' function, use that |
110 | - # to load the tests. |
111 | - if hasattr(module, "suite"): |
112 | - return module.suite() |
113 | - elif hasattr(module, "test_suite"): |
114 | - return module.test_suite() |
115 | - else: |
116 | - return unittest.defaultTestLoader.loadTestsFromModule(module) |
117 | - |
118 | - def _collect_tests(self, path, test_pattern, ignored_modules, |
119 | - ignored_paths): |
120 | - """Return the set of unittests.""" |
121 | - suite = TXCheckSuite() |
122 | - if test_pattern: |
123 | - pattern = re.compile('.*%s.*' % test_pattern) |
124 | - else: |
125 | - pattern = None |
126 | - |
127 | - # Disable this lint warning as we need to access _tests in the |
128 | - # test suites, to collect the tests |
129 | - # pylint: disable=W0212 |
130 | - if path: |
131 | - try: |
132 | - module_suite = self._load_unittest(path) |
133 | - if pattern: |
134 | - for inner_suite in module_suite._tests: |
135 | - for test in inner_suite._tests: |
136 | - if pattern.match(test.id()): |
137 | - suite.addTest(test) |
138 | - else: |
139 | - suite.addTests(module_suite) |
140 | - return suite |
141 | - except AssertionError: |
142 | - pass |
143 | - else: |
144 | - print 'Path should be defined.' |
145 | - exit(1) |
146 | - |
147 | - # We don't use the dirs variable, so ignore the warning |
148 | - # pylint: disable=W0612 |
149 | - for root, dirs, files in os.walk(path): |
150 | - for test in files: |
151 | - filepath = os.path.join(root, test) |
152 | - if test.endswith(".py") and test not in ignored_modules and \ |
153 | - not _is_in_ignored_path(filepath, ignored_paths): |
154 | - self.source_files.append(filepath) |
155 | - if test.startswith("test_"): |
156 | - module_suite = self._load_unittest(filepath) |
157 | - if pattern: |
158 | - for inner_suite in module_suite._tests: |
159 | - for test in inner_suite._tests: |
160 | - if pattern.match(test.id()): |
161 | - suite.addTest(test) |
162 | - else: |
163 | - suite.addTests(module_suite) |
164 | - return suite |
165 | - |
166 | - def get_suite(self, config): |
167 | - """Get the test suite to use.""" |
168 | - suite = unittest.TestSuite() |
169 | - for path in config['tests']: |
170 | - suite.addTest(self._collect_tests(path, config['test'], |
171 | - config['ignore-modules'], |
172 | - config['ignore-paths'])) |
173 | - if config['loop']: |
174 | - old_suite = suite |
175 | - suite = unittest.TestSuite() |
176 | - for _ in xrange(config['loop']): |
177 | - suite.addTest(old_suite) |
178 | - |
179 | - return suite |
180 | - |
181 | - # pylint: disable=C0103 |
182 | - def _runWithoutDecoration(self, test): |
183 | - """run the tests.""" |
184 | - result = None |
185 | - running_services = [] |
186 | - |
187 | - try: |
188 | - # Start any required services |
189 | - for service in self.required_services: |
190 | - runner = service() |
191 | - runner.start_service(tempdir=self.tempdir) |
192 | - running_services.append(runner) |
193 | - |
194 | - result = super(TestRunner, self)._runWithoutDecoration(test) |
195 | - finally: |
196 | - # Stop all the running services |
197 | - for runner in running_services: |
198 | - runner.stop_service() |
199 | - |
200 | - return result |
201 | - |
202 | - |
203 | -def _get_default_reactor(): |
204 | - """Return the platform-dependent default reactor to use.""" |
205 | - default_reactor = 'gi' |
206 | - if sys.platform in ['darwin', 'win32']: |
207 | - default_reactor = 'twisted' |
208 | - return default_reactor |
209 | - |
210 | - |
211 | -class Options(trial.Options): |
212 | - """Class for options handling.""" |
213 | - |
214 | - optFlags = [["coverage", "c"], |
215 | - ["gui", None], |
216 | - ["help-reactors", None], |
217 | - ] |
218 | - |
219 | - optParameters = [["test", "t", None], |
220 | - ["loop", None, 1], |
221 | - ["ignore-modules", "i", ""], |
222 | - ["ignore-paths", "p", ""], |
223 | - ["reactor", "r", _get_default_reactor()], |
224 | - ] |
225 | - |
226 | - def __init__(self): |
227 | - self['tests'] = set() |
228 | - super(Options, self).__init__() |
229 | - self['rterrors'] = True |
230 | - |
231 | - def opt_coverage(self): |
232 | - """Generate a coverage report for the run tests""" |
233 | - self['coverage'] = True |
234 | - |
235 | - def opt_gui(self): |
236 | - """Use the GUI mode of some reactors""" |
237 | - self['gui'] = True |
238 | - |
239 | - def opt_help_reactors(self): |
240 | - """Help on available reactors for use with tests""" |
241 | - synopsis = ('') |
242 | - print synopsis |
243 | - print 'Need to get list of reactors and print them here.' |
244 | |
245 | - sys.exit(0) |
246 | - |
247 | - def opt_test(self, option): |
248 | - """Run specific tests, e.g: className.methodName""" |
249 | - self['test'] = option |
250 | - |
251 | - def opt_loop(self, option): |
252 | - """Loop tests the specified number of times.""" |
253 | - try: |
254 | - self['loop'] = long(option) |
255 | - except ValueError: |
256 | - raise UsageError('A positive integer value must be specified.') |
257 | - |
258 | - def opt_ignore_modules(self, option): |
259 | - """Comma-separate list of test modules to ignore, |
260 | - e.g: test_gtk.py, test_account.py |
261 | - """ |
262 | - self['ignore-modules'] = map(str.strip, option.split(',')) |
263 | - |
264 | - def opt_ignore_paths(self, option): |
265 | - """Comma-separated list of relative paths to ignore, |
266 | - e.g: tests/platform/windows, tests/platform/macosx |
267 | - """ |
268 | - self['ignore-paths'] = map(str.strip, option.split(',')) |
269 | - |
270 | - def opt_reactor(self, option): |
271 | - """Which reactor to use (see --help-reactors for a list |
272 | - of possibilities) |
273 | - """ |
274 | - self['reactor'] = option |
275 | - opt_r = opt_reactor |
276 | - |
277 | - |
278 | -def main(): |
279 | - """Do the deed.""" |
280 | - if len(sys.argv) == 1: |
281 | - sys.argv.append('--help') |
282 | - |
283 | - config = Options() |
284 | - config.parseOptions() |
285 | - |
286 | - try: |
287 | - reactor_name = 'ubuntuone.devtools.reactors.%s' % config['reactor'] |
288 | - reactor = __import__(reactor_name, None, None, ['']) |
289 | - except ImportError: |
290 | - print 'The specified reactor is not supported.' |
291 | - sys.exit(1) |
292 | - else: |
293 | - try: |
294 | - reactor.install(options=config) |
295 | - except ImportError: |
296 | - print('The Python package providing the requested reactor is not ' |
297 | - 'installed. You can find it here: %s' % reactor.REACTOR_URL) |
298 | - raise |
299 | - |
300 | - trial_runner = TestRunner(config=config) |
301 | - suite = trial_runner.get_suite(config) |
302 | - |
303 | - if config['coverage']: |
304 | - coverage.erase() |
305 | - coverage.start() |
306 | - |
307 | - if config['until-failure']: |
308 | - result = trial_runner.runUntilFailure(suite) |
309 | - else: |
310 | - result = trial_runner.run(suite) |
311 | - |
312 | - if config['coverage']: |
313 | - coverage.stop() |
314 | - coverage.report(trial_runner.source_files, ignore_errors=True, |
315 | - show_missing=False) |
316 | - |
317 | - sys.exit(not result.wasSuccessful()) |
318 | +from ubuntuone.devtools.runners import main |
319 | |
320 | |
321 | if __name__ == '__main__': |
322 | |
323 | === modified file 'run-tests' |
324 | --- run-tests 2012-06-06 13:26:02 +0000 |
325 | +++ run-tests 2012-08-02 15:49:22 +0000 |
326 | @@ -21,6 +21,6 @@ |
327 | $PYTHON bin/u1trial --reactor=twisted -i "test_squid_windows.py" ubuntuone |
328 | echo "Running style checks..." |
329 | $PYTHON bin/u1lint |
330 | -pep8 --repeat . bin/* --exclude=*.bat |
331 | +pep8 --repeat . bin/* --exclude=*.bat,.pc |
332 | rm -rf _trial_temp |
333 | rm -rf .coverage |
334 | |
335 | === modified file 'setup.py' |
336 | --- setup.py 2012-06-05 21:28:52 +0000 |
337 | +++ setup.py 2012-08-02 15:49:22 +0000 |
338 | @@ -89,6 +89,7 @@ |
339 | packages=['ubuntuone', |
340 | 'ubuntuone.devtools', |
341 | 'ubuntuone.devtools.reactors', |
342 | + 'ubuntuone.devtools.runners', |
343 | 'ubuntuone.devtools.services', |
344 | 'ubuntuone.devtools.testing', |
345 | 'ubuntuone.devtools.testcases'], |
346 | |
347 | === added file 'ubuntuone/devtools/errors.py' |
348 | --- ubuntuone/devtools/errors.py 1970-01-01 00:00:00 +0000 |
349 | +++ ubuntuone/devtools/errors.py 2012-08-02 15:49:22 +0000 |
350 | @@ -0,0 +1,35 @@ |
351 | +# Copyright 2012 Canonical Ltd. |
352 | +# |
353 | +# This program is free software: you can redistribute it and/or modify it |
354 | +# under the terms of the GNU General Public License version 3, as published |
355 | +# by the Free Software Foundation. |
356 | +# |
357 | +# This program is distributed in the hope that it will be useful, but |
358 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
359 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
360 | +# PURPOSE. See the GNU General Public License for more details. |
361 | +# |
362 | +# You should have received a copy of the GNU General Public License along |
363 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
364 | +# |
365 | +# In addition, as a special exception, the copyright holders give |
366 | +# permission to link the code of portions of this program with the |
367 | +# OpenSSL library under certain conditions as described in each |
368 | +# individual source file, and distribute linked combinations |
369 | +# including the two. |
370 | +# You must obey the GNU General Public License in all respects |
371 | +# for all of the code used other than OpenSSL. If you modify |
372 | +# file(s) with this exception, you may extend this exception to your |
373 | +# version of the file(s), but you are not obligated to do so. If you |
374 | +# do not wish to do so, delete this exception statement from your |
375 | +# version. If you delete this exception statement from all source |
376 | +# files in the program, then also delete it here. |
377 | +"""Custom error types for Ubuntu One developer tools.""" |
378 | + |
379 | + |
380 | +class TestError(Exception): |
381 | + """An error occurred in attempting to load or start the tests.""" |
382 | + |
383 | + |
384 | +class UsageError(Exception): |
385 | + """An error occurred in parsing the command line arguments.""" |
386 | |
387 | === added directory 'ubuntuone/devtools/runners' |
388 | === added file 'ubuntuone/devtools/runners/__init__.py' |
389 | --- ubuntuone/devtools/runners/__init__.py 1970-01-01 00:00:00 +0000 |
390 | +++ ubuntuone/devtools/runners/__init__.py 2012-08-02 15:49:22 +0000 |
391 | @@ -0,0 +1,286 @@ |
392 | +# Copyright 2009-2012 Canonical Ltd. |
393 | +# |
394 | +# This program is free software: you can redistribute it and/or modify it |
395 | +# under the terms of the GNU General Public License version 3, as published |
396 | +# by the Free Software Foundation. |
397 | +# |
398 | +# This program is distributed in the hope that it will be useful, but |
399 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
400 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
401 | +# PURPOSE. See the GNU General Public License for more details. |
402 | +# |
403 | +# You should have received a copy of the GNU General Public License along |
404 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
405 | +# |
406 | +# In addition, as a special exception, the copyright holders give |
407 | +# permission to link the code of portions of this program with the |
408 | +# OpenSSL library under certain conditions as described in each |
409 | +# individual source file, and distribute linked combinations |
410 | +# including the two. |
411 | +# You must obey the GNU General Public License in all respects |
412 | +# for all of the code used other than OpenSSL. If you modify |
413 | +# file(s) with this exception, you may extend this exception to your |
414 | +# version of the file(s), but you are not obligated to do so. If you |
415 | +# do not wish to do so, delete this exception statement from your |
416 | +# version. If you delete this exception statement from all source |
417 | +# files in the program, then also delete it here. |
418 | +"""The base test runner object.""" |
419 | + |
420 | +import coverage |
421 | +import gc |
422 | +import inspect |
423 | +import os |
424 | +import re |
425 | +import sys |
426 | +import unittest |
427 | + |
428 | +from ubuntuone.devtools.errors import TestError, UsageError |
429 | +from ubuntuone.devtools.testing.txcheck import TXCheckSuite |
430 | +from ubuntuone.devtools.utils import OptionParser |
431 | + |
432 | +__all__ = ['BaseTestOptions', 'BaseTestRunner', 'main'] |
433 | + |
434 | + |
435 | +def _is_in_ignored_path(testcase, paths): |
436 | + """Return if the testcase is in one of the ignored paths.""" |
437 | + for ignored_path in paths: |
438 | + if testcase.startswith(ignored_path): |
439 | + return True |
440 | + return False |
441 | + |
442 | + |
443 | +class BaseTestRunner(object): |
444 | + """The base test runner type. Does not actually run tests.""" |
445 | + |
446 | + def __init__(self, options=None, *args, **kwargs): |
447 | + super(BaseTestRunner, self).__init__(*args, **kwargs) |
448 | + |
449 | + # set $HOME to the _trial_temp dir, to avoid breaking user files |
450 | + trial_temp_dir = os.environ.get('TRIAL_TEMP_DIR', os.getcwd()) |
451 | + homedir = os.path.join(trial_temp_dir, options['temp-directory']) |
452 | + os.environ['HOME'] = homedir |
453 | + |
454 | + # setup $XDG_*_HOME variables and create the directories |
455 | + xdg_cache = os.path.join(homedir, 'xdg_cache') |
456 | + xdg_config = os.path.join(homedir, 'xdg_config') |
457 | + xdg_data = os.path.join(homedir, 'xdg_data') |
458 | + os.environ['XDG_CACHE_HOME'] = xdg_cache |
459 | + os.environ['XDG_CONFIG_HOME'] = xdg_config |
460 | + os.environ['XDG_DATA_HOME'] = xdg_data |
461 | + |
462 | + if not os.path.exists(xdg_cache): |
463 | + os.makedirs(xdg_cache) |
464 | + if not os.path.exists(xdg_config): |
465 | + os.makedirs(xdg_config) |
466 | + if not os.path.exists(xdg_data): |
467 | + os.makedirs(xdg_data) |
468 | + |
469 | + # setup the ROOTDIR env var |
470 | + os.environ['ROOTDIR'] = os.getcwd() |
471 | + |
472 | + # Need an attribute for tempdir so we can use it later |
473 | + self.tempdir = homedir |
474 | + self.working_dir = os.path.join(self.tempdir, 'trial') |
475 | + |
476 | + self.source_files = [] |
477 | + self.required_services = [] |
478 | + |
479 | + def _load_unittest(self, relpath): |
480 | + """Load unit tests from a Python module with the given 'relpath'.""" |
481 | + assert relpath.endswith(".py"), ( |
482 | + "%s does not appear to be a Python module" % relpath) |
483 | + if not os.path.basename(relpath).startswith('test_'): |
484 | + return |
485 | + modpath = relpath.replace(os.path.sep, ".")[:-3] |
486 | + module = __import__(modpath, None, None, [""]) |
487 | + |
488 | + # If the module specifies required_services, make sure we get them |
489 | + members = [x[1] for x in inspect.getmembers(module, inspect.isclass)] |
490 | + for member_type in members: |
491 | + if hasattr(member_type, 'required_services'): |
492 | + member = member_type() |
493 | + for service in member.required_services(): |
494 | + if service not in self.required_services: |
495 | + self.required_services.append(service) |
496 | + del member |
497 | + gc.collect() |
498 | + |
499 | + # If the module has a 'suite' or 'test_suite' function, use that |
500 | + # to load the tests. |
501 | + if hasattr(module, "suite"): |
502 | + return module.suite() |
503 | + elif hasattr(module, "test_suite"): |
504 | + return module.test_suite() |
505 | + else: |
506 | + return unittest.defaultTestLoader.loadTestsFromModule(module) |
507 | + |
508 | + def _collect_tests(self, path, test_pattern, ignored_modules, |
509 | + ignored_paths): |
510 | + """Return the set of unittests.""" |
511 | + suite = TXCheckSuite() |
512 | + if test_pattern: |
513 | + pattern = re.compile('.*%s.*' % test_pattern) |
514 | + else: |
515 | + pattern = None |
516 | + |
517 | + # Disable this lint warning as we need to access _tests in the |
518 | + # test suites, to collect the tests |
519 | + # pylint: disable=W0212 |
520 | + if path: |
521 | + try: |
522 | + module_suite = self._load_unittest(path) |
523 | + if pattern: |
524 | + for inner_suite in module_suite._tests: |
525 | + for test in inner_suite._tests: |
526 | + if pattern.match(test.id()): |
527 | + suite.addTest(test) |
528 | + else: |
529 | + suite.addTests(module_suite) |
530 | + return suite |
531 | + except AssertionError: |
532 | + pass |
533 | + else: |
534 | + raise TestError('Path should be defined.') |
535 | + |
536 | + # We don't use the dirs variable, so ignore the warning |
537 | + # pylint: disable=W0612 |
538 | + for root, dirs, files in os.walk(path): |
539 | + for test in files: |
540 | + filepath = os.path.join(root, test) |
541 | + if test.endswith(".py") and test not in ignored_modules and \ |
542 | + not _is_in_ignored_path(filepath, ignored_paths): |
543 | + self.source_files.append(filepath) |
544 | + if test.startswith("test_"): |
545 | + module_suite = self._load_unittest(filepath) |
546 | + if pattern: |
547 | + for inner_suite in module_suite._tests: |
548 | + for test in inner_suite._tests: |
549 | + if pattern.match(test.id()): |
550 | + suite.addTest(test) |
551 | + else: |
552 | + suite.addTests(module_suite) |
553 | + return suite |
554 | + |
555 | + def get_suite(self, config): |
556 | + """Get the test suite to use.""" |
557 | + suite = unittest.TestSuite() |
558 | + for path in config['tests']: |
559 | + suite.addTest(self._collect_tests(path, config['test'], |
560 | + config['ignore-modules'], |
561 | + config['ignore-paths'])) |
562 | + if config['loop']: |
563 | + old_suite = suite |
564 | + suite = unittest.TestSuite() |
565 | + for _ in xrange(config['loop']): |
566 | + suite.addTest(old_suite) |
567 | + |
568 | + return suite |
569 | + |
570 | + def run_tests(self, suite): |
571 | + """Run the test suite.""" |
572 | + return False |
573 | + |
574 | + |
575 | +class BaseTestOptions(OptionParser): |
576 | + """Base options for our test runner.""" |
577 | + |
578 | + optFlags = [['coverage', 'c', 'Generate a coverage report for the tests.'], |
579 | + ['gui', None, 'Use the GUI mode of some runners.'], |
580 | + ['help', 'h', ''], |
581 | + ['help-runners', None, 'List information about test runners.'], |
582 | + ] |
583 | + |
584 | + optParameters = [['test', 't', None, None], |
585 | + ['loop', None, 1, None], |
586 | + ['ignore-modules', 'i', '', None], |
587 | + ['ignore-paths', 'p', '', None], |
588 | + ['runner', None, 'txrunner', None], |
589 | + ] |
590 | + |
591 | + def __init__(self, *args, **kwargs): |
592 | + self['tests'] = set() |
593 | + super(BaseTestOptions, self).__init__(*args, **kwargs) |
594 | + |
595 | + def opt_help_runners(self): |
596 | + """List the runners which are supported.""" |
597 | + |
598 | + def opt_ignore_modules(self, option): |
599 | + """Comma-separate list of test modules to ignore, |
600 | + e.g: test_gtk.py, test_account.py |
601 | + """ |
602 | + self['ignore-modules'] = map(str.strip, option.split(',')) |
603 | + |
604 | + def opt_ignore_paths(self, option): |
605 | + """Comma-separated list of relative paths to ignore, |
606 | + e.g: tests/platform/windows, tests/platform/macosx |
607 | + """ |
608 | + self['ignore-paths'] = map(str.strip, option.split(',')) |
609 | + |
610 | + def opt_loop(self, option): |
611 | + """Loop tests the specified number of times.""" |
612 | + try: |
613 | + self['loop'] = long(option) |
614 | + except ValueError: |
615 | + raise UsageError('A positive integer value must be specified.') |
616 | + |
617 | + def opt_test(self, option): |
618 | + """Run specific tests, e.g: className.methodName""" |
619 | + self['test'] = option |
620 | + |
621 | + |
622 | +def _get_runner_options(runner_name): |
623 | + """Return the test runner module, and its options object.""" |
624 | + module_name = 'ubuntuone.devtools.runners.%s' % runner_name |
625 | + runner = __import__(module_name, None, None, ['']) |
626 | + options = None |
627 | + if getattr(runner, 'TestOptions', None) is not None: |
628 | + options = runner.TestOptions() |
629 | + if options is None: |
630 | + options = BaseTestOptions() |
631 | + return (runner, options) |
632 | + |
633 | + |
634 | +def main(): |
635 | + """Do the deed.""" |
636 | + if len(sys.argv) == 1: |
637 | + sys.argv.append('--help') |
638 | + |
639 | + try: |
640 | + pos = sys.argv.index('--runner') |
641 | + runner_name = sys.argv.pop(pos + 1) |
642 | + sys.argv.pop(pos) |
643 | + except ValueError: |
644 | + runner_name = 'txrunner' |
645 | + finally: |
646 | + runner, options = _get_runner_options(runner_name) |
647 | + options.parseOptions() |
648 | + |
649 | + test_runner = runner.TestRunner(options=options) |
650 | + suite = test_runner.get_suite(options) |
651 | + |
652 | + if options['coverage']: |
653 | + coverage.erase() |
654 | + coverage.start() |
655 | + |
656 | + running_services = [] |
657 | + |
658 | + succeeded = False |
659 | + try: |
660 | + # Start any required services |
661 | + for service_obj in test_runner.required_services: |
662 | + service = service_obj() |
663 | + service.start_service(tempdir=test_runner.tempdir) |
664 | + running_services.append(service) |
665 | + |
666 | + succeeded = test_runner.run_tests(suite) |
667 | + finally: |
668 | + # Stop all the running services |
669 | + for service in running_services: |
670 | + service.stop_service() |
671 | + |
672 | + if options['coverage']: |
673 | + coverage.stop() |
674 | + coverage.report(test_runner.source_files, ignore_errors=True, |
675 | + show_missing=False) |
676 | + |
677 | + sys.exit(not succeeded) |
678 | |
679 | === added file 'ubuntuone/devtools/runners/txrunner.py' |
680 | --- ubuntuone/devtools/runners/txrunner.py 1970-01-01 00:00:00 +0000 |
681 | +++ ubuntuone/devtools/runners/txrunner.py 2012-08-02 15:49:22 +0000 |
682 | @@ -0,0 +1,126 @@ |
683 | +# Copyright 2009-2012 Canonical Ltd. |
684 | +# |
685 | +# This program is free software: you can redistribute it and/or modify it |
686 | +# under the terms of the GNU General Public License version 3, as published |
687 | +# by the Free Software Foundation. |
688 | +# |
689 | +# This program is distributed in the hope that it will be useful, but |
690 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
691 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
692 | +# PURPOSE. See the GNU General Public License for more details. |
693 | +# |
694 | +# You should have received a copy of the GNU General Public License along |
695 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
696 | +# |
697 | +# In addition, as a special exception, the copyright holders give |
698 | +# permission to link the code of portions of this program with the |
699 | +# OpenSSL library under certain conditions as described in each |
700 | +# individual source file, and distribute linked combinations |
701 | +# including the two. |
702 | +# You must obey the GNU General Public License in all respects |
703 | +# for all of the code used other than OpenSSL. If you modify |
704 | +# file(s) with this exception, you may extend this exception to your |
705 | +# version of the file(s), but you are not obligated to do so. If you |
706 | +# do not wish to do so, delete this exception statement from your |
707 | +# version. If you delete this exception statement from all source |
708 | +# files in the program, then also delete it here. |
709 | +"""The twisted test runner and options.""" |
710 | +import sys |
711 | + |
712 | +from twisted.scripts import trial |
713 | +from twisted.trial.runner import TrialRunner |
714 | + |
715 | +from ubuntuone.devtools.errors import TestError |
716 | +from ubuntuone.devtools.runners import BaseTestOptions, BaseTestRunner |
717 | + |
718 | +__all__ = ['TestRunner', 'TestOptions'] |
719 | + |
720 | + |
721 | +class TestRunner(BaseTestRunner, TrialRunner): |
722 | + """The twisted test runner implementation.""" |
723 | + |
724 | + def __init__(self, options=None): |
725 | + # Handle running trial in debug or dry-run mode |
726 | + self.config = options |
727 | + |
728 | + try: |
729 | + reactor_name = ('ubuntuone.devtools.reactors.%s' % |
730 | + self.config['reactor']) |
731 | + reactor = __import__(reactor_name, None, None, ['']) |
732 | + except ImportError: |
733 | + raise TestError('The specified reactor is not supported.') |
734 | + else: |
735 | + try: |
736 | + reactor.install(options=self.config) |
737 | + except ImportError: |
738 | + raise TestError( |
739 | + 'The Python package providing the requested reactor is ' |
740 | + 'not installed. You can find it here: %s' % |
741 | + reactor.REACTOR_URL) |
742 | + |
743 | + mode = None |
744 | + if self.config['debug']: |
745 | + mode = TrialRunner.DEBUG |
746 | + if self.config['dry-run']: |
747 | + mode = TrialRunner.DRY_RUN |
748 | + |
749 | + # Hook up to the parent test runner |
750 | + super(TestRunner, self).__init__( |
751 | + options=options, |
752 | + reporterFactory=self.config['reporter'], |
753 | + mode=mode, |
754 | + profile=self.config['profile'], |
755 | + logfile=self.config['logfile'], |
756 | + tracebackFormat=self.config['tbformat'], |
757 | + realTimeErrors=self.config['rterrors'], |
758 | + uncleanWarnings=self.config['unclean-warnings'], |
759 | + forceGarbageCollection=self.config['force-gc']) |
760 | + # Named for trial compatibility. |
761 | + # pylint: disable=C0103 |
762 | + self.workingDirectory = self.working_dir |
763 | + # pylint: enable=C0103 |
764 | + |
765 | + def run_tests(self, suite): |
766 | + """Run the twisted test suite.""" |
767 | + if self.config['until-failure']: |
768 | + result = self.runUntilFailure(suite) |
769 | + else: |
770 | + result = self.run(suite) |
771 | + return result.wasSuccessful() |
772 | + |
773 | + |
774 | +def _get_default_reactor(): |
775 | + """Return the platform-dependent default reactor to use.""" |
776 | + default_reactor = 'gi' |
777 | + if sys.platform in ['darwin', 'win32']: |
778 | + default_reactor = 'twisted' |
779 | + return default_reactor |
780 | + |
781 | + |
782 | +class TestOptions(trial.Options, BaseTestOptions): |
783 | + """Class for twisted options handling.""" |
784 | + |
785 | + optFlags = [["help-reactors", None], |
786 | + ] |
787 | + |
788 | + optParameters = [["reactor", "r", _get_default_reactor()], |
789 | + ] |
790 | + |
791 | + def __init__(self, *args, **kwargs): |
792 | + super(TestOptions, self).__init__(*args, **kwargs) |
793 | + self['rterrors'] = True |
794 | + |
795 | + def opt_help_reactors(self): |
796 | + """Help on available reactors for use with tests""" |
797 | + synopsis = ('') |
798 | + print(synopsis) |
799 | + print('Need to get list of reactors and print them here.') |
800 | + print() |
801 | + sys.exit(0) |
802 | + |
803 | + def opt_reactor(self, option): |
804 | + """Which reactor to use (see --help-reactors for a list |
805 | + of possibilities) |
806 | + """ |
807 | + self['reactor'] = option |
808 | + opt_r = opt_reactor |
809 | |
810 | === modified file 'ubuntuone/devtools/testcases/dbus.py' |
811 | --- ubuntuone/devtools/testcases/dbus.py 2012-03-30 17:44:03 +0000 |
812 | +++ ubuntuone/devtools/testcases/dbus.py 2012-08-02 15:49:22 +0000 |
813 | @@ -97,7 +97,7 @@ |
814 | bus_address = os.environ.get('DBUS_SESSION_BUS_ADDRESS', None) |
815 | if os.path.dirname(unquote(bus_address.split(',')[0].split('=')[1])) \ |
816 | != os.path.dirname(os.getcwd()): |
817 | - raise InvalidSessionBus('DBUS_SESSION_BUS_ADDRES is wrong.') |
818 | + raise InvalidSessionBus('DBUS_SESSION_BUS_ADDRESS is wrong.') |
819 | |
820 | # Set up the main loop and bus connection |
821 | self.loop = DBusGMainLoop(set_as_default=True) |
822 | |
823 | === added file 'ubuntuone/devtools/utils.py' |
824 | --- ubuntuone/devtools/utils.py 1970-01-01 00:00:00 +0000 |
825 | +++ ubuntuone/devtools/utils.py 2012-08-02 15:49:22 +0000 |
826 | @@ -0,0 +1,167 @@ |
827 | +# Copyright 2009-2012 Canonical Ltd. |
828 | +# |
829 | +# This program is free software: you can redistribute it and/or modify it |
830 | +# under the terms of the GNU General Public License version 3, as published |
831 | +# by the Free Software Foundation. |
832 | +# |
833 | +# This program is distributed in the hope that it will be useful, but |
834 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
835 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
836 | +# PURPOSE. See the GNU General Public License for more details. |
837 | +# |
838 | +# You should have received a copy of the GNU General Public License along |
839 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
840 | +# |
841 | +# In addition, as a special exception, the copyright holders give |
842 | +# permission to link the code of portions of this program with the |
843 | +# OpenSSL library under certain conditions as described in each |
844 | +# individual source file, and distribute linked combinations |
845 | +# including the two. |
846 | +# You must obey the GNU General Public License in all respects |
847 | +# for all of the code used other than OpenSSL. If you modify |
848 | +# file(s) with this exception, you may extend this exception to your |
849 | +# version of the file(s), but you are not obligated to do so. If you |
850 | +# do not wish to do so, delete this exception statement from your |
851 | +# version. If you delete this exception statement from all source |
852 | +# files in the program, then also delete it here. |
853 | +"""Utilities for Ubuntu One developer tools.""" |
854 | + |
855 | +import getopt |
856 | +import sys |
857 | + |
858 | +from ubuntuone.devtools.errors import UsageError |
859 | +__all__ = ['OptionParser'] |
860 | + |
861 | + |
862 | +def accumulate_list_attr(class_obj, attr, list_obj, base_class=None): |
863 | + """Get all of the list attributes of attr from the class hierarchy, |
864 | + and return a list of the lists.""" |
865 | + for base in class_obj.__bases__: |
866 | + accumulate_list_attr(base, attr, list_obj) |
867 | + if base_class is None or base_class in class_obj.__bases__: |
868 | + list_obj.extend(class_obj.__dict__.get(attr, [])) |
869 | + |
870 | + |
871 | +def unpack_padded(length, sequence, default=None): |
872 | + """Pads a sequence to length with value of default. |
873 | + |
874 | + Returns a list containing the original and padded values. |
875 | + """ |
876 | + newlist = [default] * length |
877 | + newlist[:len(sequence)] = list(sequence) |
878 | + return newlist |
879 | + |
880 | + |
881 | +class OptionParser(dict): |
882 | + """Base options for our test runner.""" |
883 | + |
884 | + def __init__(self, *args, **kwargs): |
885 | + super(OptionParser, self).__init__(*args, **kwargs) |
886 | + |
887 | + # Store info about the options and defaults |
888 | + self.long_opts = [] |
889 | + self.short_opts = '' |
890 | + self.docs = {} |
891 | + self.defaults = {} |
892 | + self.dispatch = {} |
893 | + |
894 | + # Get the options and defaults |
895 | + for _get in [self._get_flags, self._get_params]: |
896 | + # We don't use variable 'syns' here. It's just to pad the result. |
897 | + # pylint: disable=W0612 |
898 | + (long_opts, short_opts, docs, defaults, syns, dispatch) = _get() |
899 | + # pylint: enable=W0612 |
900 | + self.long_opts.extend(long_opts) |
901 | + self.short_opts = self.short_opts + short_opts |
902 | + self.docs.update(docs) |
903 | + self.update(defaults) |
904 | + self.defaults.update(defaults) |
905 | + self.dispatch.update(dispatch) |
906 | + |
907 | + # We use some camelcase names for trial compatibility here. |
908 | + # pylint: disable=C0103 |
909 | + def parseOptions(self, options=None): |
910 | + """Parse the options.""" |
911 | + if options is None: |
912 | + options = sys.argv[1:] |
913 | + |
914 | + try: |
915 | + opts, args = getopt.getopt(options, |
916 | + self.short_opts, self.long_opts) |
917 | + except getopt.error as e: |
918 | + raise UsageError(e) |
919 | + |
920 | + for opt, arg in opts: |
921 | + if opt[1] == '-': |
922 | + opt = opt[2:] |
923 | + else: |
924 | + opt = opt[1:] |
925 | + |
926 | + if (opt not in self.long_opts and |
927 | + (len(opt) == 1 and opt not in self.short_opts)): |
928 | + raise UsageError('No such options: "%s"' % opt) |
929 | + |
930 | + self.dispatch[opt](arg) |
931 | + |
932 | + try: |
933 | + self.parseArgs(*args) |
934 | + except TypeError: |
935 | + raise UsageError('Wrong number of arguments.') |
936 | + |
937 | + self.postOptions() |
938 | + |
939 | + def postOptions(self): |
940 | + """Called after options are parsed.""" |
941 | + |
942 | + def parseArgs(self, *args): |
943 | + """Override to handle extra arguments specially.""" |
944 | + # pylint: enable=C0103 |
945 | + |
946 | + def _parse_arguments(self, has_default=False): |
947 | + """Parse the arguments as either flags or parameters.""" |
948 | + long_opts, short_opts = [], '' |
949 | + docs, defaults, dispatch = {}, {}, {} |
950 | + |
951 | + _args = [] |
952 | + accumulate_list_attr(self.__class__, 'optFlags', _args) |
953 | + |
954 | + for _arg in _args: |
955 | + try: |
956 | + if has_default: |
957 | + l_opt, s_opt, default, doc = unpack_padded(4, _arg) |
958 | + else: |
959 | + default = False |
960 | + l_opt, s_opt, doc = unpack_padded(3, _arg) |
961 | + except ValueError: |
962 | + raise ValueError('Failed to parse argument: %s' % _arg) |
963 | + if not l_opt: |
964 | + raise ValueError('An option must have a long name.') |
965 | + |
966 | + opt_m_name = 'opt_' + l_opt.replace('-', '_') |
967 | + opt_method = getattr(self, opt_m_name, None) |
968 | + if opt_method is not None: |
969 | + docs[l_opt] = getattr(opt_method, '__doc__', None) |
970 | + dispatch[l_opt] = opt_method |
971 | + if docs[l_opt] is None: |
972 | + docs[l_opt] = doc |
973 | + else: |
974 | + docs[l_opt] = doc |
975 | + dispatch[l_opt] = lambda arg: True |
976 | + |
977 | + defaults[l_opt] = default |
978 | + long_opts.append(l_opt) |
979 | + if s_opt is not None: |
980 | + short_opts = short_opts + s_opt |
981 | + docs[s_opt] = docs[l_opt] |
982 | + dispatch[s_opt] = dispatch[l_opt] |
983 | + defaults[s_opt] = defaults[l_opt] |
984 | + |
985 | + return long_opts, short_opts, docs, defaults, None, dispatch |
986 | + |
987 | + def _get_flags(self): |
988 | + """Get the flag options.""" |
989 | + return self._parse_arguments(has_default=False) |
990 | + |
991 | + def _get_params(self): |
992 | + """Get the parameters options.""" |
993 | + return self._parse_arguments(has_default=True) |
_get_flags and _get_params are too much alike, I think the common code should be merged.
There are no tests for the new code. Please create a new bug for that.
The code looks fine otherwise.