Merge lp:~dobey/ubuntu/precise/ubuntuone-dev-tools/release-2-99-0 into lp:ubuntu/precise/ubuntuone-dev-tools
- Precise (12.04)
- release-2-99-0
- Merge into precise
Proposed by
dobey
Status: | Merged |
---|---|
Merged at revision: | 6 |
Proposed branch: | lp:~dobey/ubuntu/precise/ubuntuone-dev-tools/release-2-99-0 |
Merge into: | lp:ubuntu/precise/ubuntuone-dev-tools |
Diff against target: |
1560 lines (+869/-366) 17 files modified
PKG-INFO (+1/-1) bin/u1lint (+10/-5) bin/u1trial (+142/-86) debian/changelog (+20/-0) debian/control (+10/-9) debian/pycompat (+0/-1) debian/rules (+6/-11) run-tests (+3/-2) setup.py (+4/-2) ubuntuone/devtools/reactors/glib.py (+1/-1) ubuntuone/devtools/reactors/qt4.py (+9/-2) ubuntuone/devtools/services/dbus.py (+1/-1) ubuntuone/devtools/testcase.py (+11/-245) ubuntuone/devtools/testcases/__init__.py (+166/-0) ubuntuone/devtools/testcases/dbus.py (+118/-0) ubuntuone/devtools/testing/__init__.py (+1/-0) ubuntuone/devtools/testing/txcheck.py (+366/-0) |
To merge this branch: | bzr merge lp:~dobey/ubuntu/precise/ubuntuone-dev-tools/release-2-99-0 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ken VanDine | Approve | ||
Ubuntu branches | Pending | ||
Review via email:
|
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Daniel Holbach (dholbach) wrote : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'PKG-INFO' | |||
2 | --- PKG-INFO 2011-09-13 14:48:46 +0000 | |||
3 | +++ PKG-INFO 2012-01-04 20:35:34 +0000 | |||
4 | @@ -1,6 +1,6 @@ | |||
5 | 1 | Metadata-Version: 1.0 | 1 | Metadata-Version: 1.0 |
6 | 2 | Name: ubuntuone-dev-tools | 2 | Name: ubuntuone-dev-tools |
8 | 3 | Version: 0.2.0 | 3 | Version: 2.99.1 |
9 | 4 | Summary: Ubuntu One development tools and utilities | 4 | Summary: Ubuntu One development tools and utilities |
10 | 5 | Home-page: http://launchpad.net/ubuntuone-dev-tools | 5 | Home-page: http://launchpad.net/ubuntuone-dev-tools |
11 | 6 | Author: UNKNOWN | 6 | Author: UNKNOWN |
12 | 7 | 7 | ||
13 | === modified file 'bin/u1lint' | |||
14 | --- bin/u1lint 2011-09-13 14:48:46 +0000 | |||
15 | +++ bin/u1lint 2012-01-04 20:35:34 +0000 | |||
16 | @@ -4,7 +4,7 @@ | |||
17 | 4 | # | 4 | # |
18 | 5 | # Author: Rodney Dawes <rodney.dawes@canonical.com> | 5 | # Author: Rodney Dawes <rodney.dawes@canonical.com> |
19 | 6 | # | 6 | # |
21 | 7 | # Copyright 2009-2010 Canonical Ltd. | 7 | # Copyright 2009-2011 Canonical Ltd. |
22 | 8 | # | 8 | # |
23 | 9 | # This program is free software: you can redistribute it and/or modify it | 9 | # This program is free software: you can redistribute it and/or modify it |
24 | 10 | # under the terms of the GNU General Public License version 3, as published | 10 | # under the terms of the GNU General Public License version 3, as published |
25 | @@ -25,7 +25,7 @@ | |||
26 | 25 | import subprocess | 25 | import subprocess |
27 | 26 | import sys | 26 | import sys |
28 | 27 | 27 | ||
30 | 28 | from xdg.BaseDirectory import xdg_data_dirs | 28 | from dirspec.basedir import xdg_data_dirs |
31 | 29 | 29 | ||
32 | 30 | SRCDIR = os.environ.get('SRCDIR', os.getcwd()) | 30 | SRCDIR = os.environ.get('SRCDIR', os.getcwd()) |
33 | 31 | 31 | ||
34 | @@ -33,7 +33,7 @@ | |||
35 | 33 | class InvalidSetupException(Exception): | 33 | class InvalidSetupException(Exception): |
36 | 34 | """Raised when the env is not correctly setup.""" | 34 | """Raised when the env is not correctly setup.""" |
37 | 35 | 35 | ||
39 | 36 | 36 | ||
40 | 37 | def find_python_installation_path(): | 37 | def find_python_installation_path(): |
41 | 38 | """Return the path where python was installed.""" | 38 | """Return the path where python was installed.""" |
42 | 39 | assert(sys.platform == 'win32') | 39 | assert(sys.platform == 'win32') |
43 | @@ -98,7 +98,8 @@ | |||
44 | 98 | # the default is to assume that the script is executable and that it | 98 | # the default is to assume that the script is executable and that it |
45 | 99 | # can be found in the path | 99 | # can be found in the path |
46 | 100 | return [script, ] | 100 | return [script, ] |
48 | 101 | 101 | ||
49 | 102 | |||
50 | 102 | def find_pylintrc(): | 103 | def find_pylintrc(): |
51 | 103 | """Return the first pylintrc found.""" | 104 | """Return the first pylintrc found.""" |
52 | 104 | # Use the pylintrc in the source tree if there is one | 105 | # Use the pylintrc in the source tree if there is one |
53 | @@ -114,8 +115,10 @@ | |||
54 | 114 | return full_name | 115 | return full_name |
55 | 115 | return None | 116 | return None |
56 | 116 | 117 | ||
57 | 118 | |||
58 | 117 | PYLINTRC = find_pylintrc() | 119 | PYLINTRC = find_pylintrc() |
59 | 118 | 120 | ||
60 | 121 | |||
61 | 119 | def _read_pylintrc_ignored(): | 122 | def _read_pylintrc_ignored(): |
62 | 120 | """Get the ignored files list from pylintrc""" | 123 | """Get the ignored files list from pylintrc""" |
63 | 121 | try: | 124 | try: |
64 | @@ -129,6 +132,7 @@ | |||
65 | 129 | return [] | 132 | return [] |
66 | 130 | # pylint: enable=E1103 | 133 | # pylint: enable=E1103 |
67 | 131 | 134 | ||
68 | 135 | |||
69 | 132 | def _group_lines_by_file(data): | 136 | def _group_lines_by_file(data): |
70 | 133 | """Format file:line:message output as lines grouped by file.""" | 137 | """Format file:line:message output as lines grouped by file.""" |
71 | 134 | did_fail = False | 138 | did_fail = False |
72 | @@ -156,6 +160,7 @@ | |||
73 | 156 | 160 | ||
74 | 157 | return (did_fail, "\n".join(outputs)) | 161 | return (did_fail, "\n".join(outputs)) |
75 | 158 | 162 | ||
76 | 163 | |||
77 | 159 | def _find_files(): | 164 | def _find_files(): |
78 | 160 | """Find all Python files under the current tree.""" | 165 | """Find all Python files under the current tree.""" |
79 | 161 | pyfiles = [] | 166 | pyfiles = [] |
80 | @@ -199,7 +204,7 @@ | |||
81 | 199 | else: | 204 | else: |
82 | 200 | pylint_args = get_subprocess_start_info('pylint') | 205 | pylint_args = get_subprocess_start_info('pylint') |
83 | 201 | # append the extra args to the start info | 206 | # append the extra args to the start info |
85 | 202 | pylint_args.extend(['--output-format=parseable', | 207 | pylint_args.extend(['--output-format=parseable', |
86 | 203 | '--include-ids=yes']) | 208 | '--include-ids=yes']) |
87 | 204 | if PYLINTRC: | 209 | if PYLINTRC: |
88 | 205 | pylint_args.append("--rcfile=" + PYLINTRC) | 210 | pylint_args.append("--rcfile=" + PYLINTRC) |
89 | 206 | 211 | ||
90 | === modified file 'bin/u1trial' | |||
91 | --- bin/u1trial 2011-09-13 14:48:46 +0000 | |||
92 | +++ bin/u1trial 2012-01-04 20:35:34 +0000 | |||
93 | @@ -4,7 +4,7 @@ | |||
94 | 4 | # | 4 | # |
95 | 5 | # Author: Rodney Dawes <rodney.dawes@canonical.com> | 5 | # Author: Rodney Dawes <rodney.dawes@canonical.com> |
96 | 6 | # | 6 | # |
98 | 7 | # Copyright 2009-2010 Canonical Ltd. | 7 | # Copyright 2009-2011 Canonical Ltd. |
99 | 8 | # | 8 | # |
100 | 9 | # This program is free software: you can redistribute it and/or modify it | 9 | # This program is free software: you can redistribute it and/or modify it |
101 | 10 | # under the terms of the GNU General Public License version 3, as published | 10 | # under the terms of the GNU General Public License version 3, as published |
102 | @@ -28,11 +28,14 @@ | |||
103 | 28 | import sys | 28 | import sys |
104 | 29 | import unittest | 29 | import unittest |
105 | 30 | 30 | ||
106 | 31 | from twisted.python.usage import UsageError | ||
107 | 32 | from twisted.scripts import trial | ||
108 | 31 | from twisted.trial.runner import TrialRunner | 33 | from twisted.trial.runner import TrialRunner |
109 | 32 | 34 | ||
110 | 33 | |||
111 | 34 | sys.path.insert(0, os.path.abspath(".")) | 35 | sys.path.insert(0, os.path.abspath(".")) |
112 | 35 | 36 | ||
113 | 37 | from ubuntuone.devtools.testing.txcheck import TXCheckSuite | ||
114 | 38 | |||
115 | 36 | 39 | ||
116 | 37 | def _is_in_ignored_path(testcase, paths): | 40 | def _is_in_ignored_path(testcase, paths): |
117 | 38 | """Return if the testcase is in one of the ignored paths.""" | 41 | """Return if the testcase is in one of the ignored paths.""" |
118 | @@ -45,12 +48,10 @@ | |||
119 | 45 | class TestRunner(TrialRunner): | 48 | class TestRunner(TrialRunner): |
120 | 46 | """The test runner implementation.""" | 49 | """The test runner implementation.""" |
121 | 47 | 50 | ||
125 | 48 | def __init__(self, force_gc=False): | 51 | def __init__(self, config=None): |
123 | 49 | from twisted.trial.reporter import TreeReporter | ||
124 | 50 | |||
126 | 51 | # set $HOME to the _trial_temp dir, to avoid breaking user files | 52 | # set $HOME to the _trial_temp dir, to avoid breaking user files |
127 | 52 | trial_temp_dir = os.environ.get('TRIAL_TEMP_DIR', os.getcwd()) | 53 | trial_temp_dir = os.environ.get('TRIAL_TEMP_DIR', os.getcwd()) |
129 | 53 | homedir = os.path.join(trial_temp_dir, '_trial_temp') | 54 | homedir = os.path.join(trial_temp_dir, config['temp-directory']) |
130 | 54 | os.environ['HOME'] = homedir | 55 | os.environ['HOME'] = homedir |
131 | 55 | 56 | ||
132 | 56 | # setup $XDG_*_HOME variables and create the directories | 57 | # setup $XDG_*_HOME variables and create the directories |
133 | @@ -71,12 +72,29 @@ | |||
134 | 71 | # setup the ROOTDIR env var | 72 | # setup the ROOTDIR env var |
135 | 72 | os.environ['ROOTDIR'] = os.getcwd() | 73 | os.environ['ROOTDIR'] = os.getcwd() |
136 | 73 | 74 | ||
137 | 75 | # Need an attribute for tempdir so we can use it later | ||
138 | 74 | self.tempdir = homedir | 76 | self.tempdir = homedir |
144 | 75 | working_dir = os.path.join(self.tempdir, 'tmp') | 77 | working_dir = os.path.join(self.tempdir, 'trial') |
145 | 76 | super(TestRunner, self).__init__(reporterFactory=TreeReporter, | 78 | |
146 | 77 | realTimeErrors=True, | 79 | # Handle running trial in debug or dry-run mode |
147 | 78 | workingDirectory=working_dir, | 80 | mode = None |
148 | 79 | forceGarbageCollection=force_gc) | 81 | if config['debug']: |
149 | 82 | mode = TrialRunner.DEBUG | ||
150 | 83 | if config['dry-run']: | ||
151 | 84 | mode = TrialRunner.DRY_RUN | ||
152 | 85 | |||
153 | 86 | # Hook up to the parent test runner | ||
154 | 87 | super(TestRunner, self).__init__( | ||
155 | 88 | reporterFactory=config['reporter'], | ||
156 | 89 | mode=mode, | ||
157 | 90 | profile=config['profile'], | ||
158 | 91 | logfile=config['logfile'], | ||
159 | 92 | tracebackFormat=config['tbformat'], | ||
160 | 93 | realTimeErrors=config['rterrors'], | ||
161 | 94 | uncleanWarnings=config['unclean-warnings'], | ||
162 | 95 | workingDirectory=working_dir, | ||
163 | 96 | forceGarbageCollection=config['force-gc']) | ||
164 | 97 | |||
165 | 80 | self.required_services = [] | 98 | self.required_services = [] |
166 | 81 | self.source_files = [] | 99 | self.source_files = [] |
167 | 82 | 100 | ||
168 | @@ -112,23 +130,11 @@ | |||
169 | 112 | def _collect_tests(self, path, test_pattern, ignored_modules, | 130 | def _collect_tests(self, path, test_pattern, ignored_modules, |
170 | 113 | ignored_paths): | 131 | ignored_paths): |
171 | 114 | """Return the set of unittests.""" | 132 | """Return the set of unittests.""" |
173 | 115 | suite = unittest.TestSuite() | 133 | suite = TXCheckSuite() |
174 | 116 | if test_pattern: | 134 | if test_pattern: |
175 | 117 | pattern = re.compile('.*%s.*' % test_pattern) | 135 | pattern = re.compile('.*%s.*' % test_pattern) |
176 | 118 | else: | 136 | else: |
177 | 119 | pattern = None | 137 | pattern = None |
178 | 120 | |||
179 | 121 | # get the ignored modules/tests | ||
180 | 122 | if ignored_modules: | ||
181 | 123 | ignored_modules = map(str.strip, ignored_modules.split(',')) | ||
182 | 124 | else: | ||
183 | 125 | ignored_modules = [] | ||
184 | 126 | |||
185 | 127 | # get the ignored paths | ||
186 | 128 | if ignored_paths: | ||
187 | 129 | ignored_paths = map(str.strip, ignored_paths.split(',')) | ||
188 | 130 | else: | ||
189 | 131 | ignored_paths = [] | ||
190 | 132 | 138 | ||
191 | 133 | # Disable this lint warning as we need to access _tests in the | 139 | # Disable this lint warning as we need to access _tests in the |
192 | 134 | # test suites, to collect the tests | 140 | # test suites, to collect the tests |
193 | @@ -169,100 +175,150 @@ | |||
194 | 169 | suite.addTests(module_suite) | 175 | suite.addTests(module_suite) |
195 | 170 | return suite | 176 | return suite |
196 | 171 | 177 | ||
199 | 172 | # pylint: disable=E0202 | 178 | def get_suite(self, config): |
200 | 173 | def run(self, args, options=None): | 179 | """Get the test suite to use.""" |
201 | 180 | suite = unittest.TestSuite() | ||
202 | 181 | for path in config['tests']: | ||
203 | 182 | suite.addTest(self._collect_tests(path, config['test'], | ||
204 | 183 | config['ignore-modules'], | ||
205 | 184 | config['ignore-paths'])) | ||
206 | 185 | if config['loop']: | ||
207 | 186 | old_suite = suite | ||
208 | 187 | suite = unittest.TestSuite() | ||
209 | 188 | for _ in xrange(config['loop']): | ||
210 | 189 | suite.addTest(old_suite) | ||
211 | 190 | |||
212 | 191 | return suite | ||
213 | 192 | |||
214 | 193 | # pylint: disable=C0103 | ||
215 | 194 | def _runWithoutDecoration(self, test): | ||
216 | 174 | """run the tests.""" | 195 | """run the tests.""" |
218 | 175 | success = 0 | 196 | result = None |
219 | 176 | running_services = [] | 197 | running_services = [] |
220 | 177 | if options.coverage: | ||
221 | 178 | coverage.erase() | ||
222 | 179 | coverage.start() | ||
223 | 180 | 198 | ||
224 | 181 | try: | 199 | try: |
225 | 182 | suite = unittest.TestSuite() | ||
226 | 183 | for path in args: | ||
227 | 184 | print "Adding path" | ||
228 | 185 | suite.addTest(self._collect_tests(path, options.test, | ||
229 | 186 | options.ignored_modules, | ||
230 | 187 | options.ignored_paths)) | ||
231 | 188 | if options.loops: | ||
232 | 189 | old_suite = suite | ||
233 | 190 | suite = unittest.TestSuite() | ||
234 | 191 | for _ in xrange(options.loops): | ||
235 | 192 | suite.addTest(old_suite) | ||
236 | 193 | |||
237 | 194 | # Start any required services | 200 | # Start any required services |
238 | 195 | for service in self.required_services: | 201 | for service in self.required_services: |
239 | 196 | runner = service() | 202 | runner = service() |
240 | 197 | runner.start_service(tempdir=self.tempdir) | 203 | runner.start_service(tempdir=self.tempdir) |
241 | 198 | running_services.append(runner) | 204 | running_services.append(runner) |
242 | 199 | 205 | ||
245 | 200 | result = super(TestRunner, self).run(suite) | 206 | result = super(TestRunner, self)._runWithoutDecoration(test) |
244 | 201 | success = result.wasSuccessful() | ||
246 | 202 | finally: | 207 | finally: |
247 | 203 | # Stop all the running services | 208 | # Stop all the running services |
248 | 204 | for runner in running_services: | 209 | for runner in running_services: |
249 | 205 | runner.stop_service() | 210 | runner.stop_service() |
250 | 206 | 211 | ||
260 | 207 | if options.coverage: | 212 | return result |
261 | 208 | coverage.stop() | 213 | |
262 | 209 | coverage.report(self.source_files, ignore_errors=True, | 214 | |
263 | 210 | show_missing=False) | 215 | class Options(trial.Options): |
264 | 211 | 216 | """Class for options handling.""" | |
265 | 212 | if not success: | 217 | |
266 | 213 | sys.exit(1) | 218 | optFlags = [["coverage", "c"], |
267 | 214 | else: | 219 | ["gui", None], |
268 | 215 | sys.exit(0) | 220 | ["help-reactors", None], |
269 | 221 | ] | ||
270 | 222 | |||
271 | 223 | optParameters = [["test", "t", None], | ||
272 | 224 | ["loop", None, 1], | ||
273 | 225 | ["ignore-modules", "i", ""], | ||
274 | 226 | ["ignore-paths", "p", ""], | ||
275 | 227 | ["reactor", "r", "glib"], | ||
276 | 228 | ] | ||
277 | 229 | |||
278 | 230 | def __init__(self): | ||
279 | 231 | self['tests'] = set() | ||
280 | 232 | super(Options, self).__init__() | ||
281 | 233 | self['rterrors'] = True | ||
282 | 234 | |||
283 | 235 | def opt_coverage(self): | ||
284 | 236 | """Generate a coverage report for the run tests""" | ||
285 | 237 | self['coverage'] = True | ||
286 | 238 | |||
287 | 239 | def opt_gui(self): | ||
288 | 240 | """Use the GUI mode of some reactors""" | ||
289 | 241 | self['gui'] = True | ||
290 | 242 | |||
291 | 243 | def opt_help_reactors(self): | ||
292 | 244 | """Help on available reactors for use with tests""" | ||
293 | 245 | synopsis = ('') | ||
294 | 246 | print synopsis | ||
295 | 247 | print 'Need to get list of reactors and print them here.' | ||
296 | 248 | |||
297 | 249 | sys.exit(0) | ||
298 | 250 | |||
299 | 251 | def opt_test(self, option): | ||
300 | 252 | """Run specific tests, e.g: className.methodName""" | ||
301 | 253 | self['test'] = option | ||
302 | 254 | |||
303 | 255 | def opt_loop(self, option): | ||
304 | 256 | """Loop tests the specified number of times.""" | ||
305 | 257 | try: | ||
306 | 258 | self['loop'] = long(option) | ||
307 | 259 | except ValueError: | ||
308 | 260 | raise UsageError('A positive integer value must be specified.') | ||
309 | 261 | |||
310 | 262 | def opt_ignore_modules(self, option): | ||
311 | 263 | """Comma-separate list of test modules to ignore, | ||
312 | 264 | e.g: test_gtk.py, test_account.py | ||
313 | 265 | """ | ||
314 | 266 | self['ignore-modules'] = map(str.strip, option.split(',')) | ||
315 | 267 | |||
316 | 268 | def opt_ignore_paths(self, option): | ||
317 | 269 | """Comma-separated list of relative paths to ignore, | ||
318 | 270 | e.g: tests/platform/windows, tests/platform/macosx | ||
319 | 271 | """ | ||
320 | 272 | self['ignore-paths'] = map(str.strip, option.split(',')) | ||
321 | 273 | |||
322 | 274 | def opt_reactor(self, option): | ||
323 | 275 | """Which reactor to use (see --help-reactors for a list | ||
324 | 276 | of possibilities) | ||
325 | 277 | """ | ||
326 | 278 | self['reactor'] = option | ||
327 | 279 | opt_r = opt_reactor | ||
328 | 216 | 280 | ||
329 | 217 | 281 | ||
330 | 218 | def main(): | 282 | def main(): |
331 | 219 | """Do the deed.""" | 283 | """Do the deed.""" |
362 | 220 | from optparse import OptionParser | 284 | if len(sys.argv) == 1: |
363 | 221 | usage = '%prog [options] path' | 285 | sys.argv.append('--help') |
364 | 222 | parser = OptionParser(usage=usage) | 286 | |
365 | 223 | parser.add_option("-t", "--test", dest="test", | 287 | config = Options() |
366 | 224 | help = "run specific tests, e.g: className.methodName") | 288 | config.parseOptions() |
337 | 225 | parser.add_option("-l", "--loop", dest="loops", type="int", default=1, | ||
338 | 226 | help = "loop selected tests LOOPS number of times", | ||
339 | 227 | metavar="LOOPS") | ||
340 | 228 | parser.add_option("-c", "--coverage", action="store_true", dest="coverage", | ||
341 | 229 | help="print a coverage report when finished") | ||
342 | 230 | parser.add_option("-i", "--ignored-modules", dest="ignored_modules", | ||
343 | 231 | default=None, help="comma-separated test moodules " | ||
344 | 232 | + "to ignore, e.g: test_gtk.py, test_account.py") | ||
345 | 233 | parser.add_option("-p", "--ignore-paths", dest="ignored_paths", | ||
346 | 234 | default=None, help="comma-separated relative " | ||
347 | 235 | + "paths to ignore. " | ||
348 | 236 | + "e.g: tests/platform/windows, tests/platform/macosx") | ||
349 | 237 | parser.add_option("--force-gc", action="store_true", dest="force_gc", | ||
350 | 238 | default=False, help="Run gc.collect() before and after " | ||
351 | 239 | "each test case.") | ||
352 | 240 | parser.add_option("--reactor", type="string", dest="reactor", | ||
353 | 241 | default='glib', | ||
354 | 242 | help="Run the tests using the specified reactor.", | ||
355 | 243 | metavar="REACTOR") | ||
356 | 244 | parser.add_option("--gui", action="store_true", dest="use_gui", | ||
357 | 245 | help="Use the GUI mode of some reactors.") | ||
358 | 246 | (options, args) = parser.parse_args() | ||
359 | 247 | if not args: | ||
360 | 248 | parser.print_help() | ||
361 | 249 | sys.exit(2) | ||
367 | 250 | 289 | ||
368 | 251 | try: | 290 | try: |
370 | 252 | reactor_name = 'ubuntuone.devtools.reactors.%s' % options.reactor | 291 | reactor_name = 'ubuntuone.devtools.reactors.%s' % config['reactor'] |
371 | 253 | reactor = __import__(reactor_name, None, None, ['']) | 292 | reactor = __import__(reactor_name, None, None, ['']) |
372 | 254 | except ImportError: | 293 | except ImportError: |
373 | 255 | print 'The specified reactor is not supported.' | 294 | print 'The specified reactor is not supported.' |
374 | 256 | sys.exit(1) | 295 | sys.exit(1) |
375 | 257 | else: | 296 | else: |
376 | 258 | try: | 297 | try: |
378 | 259 | reactor.install(options=options) | 298 | reactor.install(options=config) |
379 | 260 | except ImportError: | 299 | except ImportError: |
380 | 261 | print('The Python package providing the requested reactor is not ' | 300 | print('The Python package providing the requested reactor is not ' |
381 | 262 | 'installed. You can find it here: %s' % reactor.REACTOR_URL) | 301 | 'installed. You can find it here: %s' % reactor.REACTOR_URL) |
382 | 263 | raise | 302 | raise |
383 | 264 | 303 | ||
385 | 265 | TestRunner(force_gc=options.force_gc).run(args, options) | 304 | trial_runner = TestRunner(config=config) |
386 | 305 | suite = trial_runner.get_suite(config) | ||
387 | 306 | |||
388 | 307 | if config['coverage']: | ||
389 | 308 | coverage.erase() | ||
390 | 309 | coverage.start() | ||
391 | 310 | |||
392 | 311 | if config['until-failure']: | ||
393 | 312 | result = trial_runner.runUntilFailure(suite) | ||
394 | 313 | else: | ||
395 | 314 | result = trial_runner.run(suite) | ||
396 | 315 | |||
397 | 316 | if config['coverage']: | ||
398 | 317 | coverage.stop() | ||
399 | 318 | coverage.report(trial_runner.source_files, ignore_errors=True, | ||
400 | 319 | show_missing=False) | ||
401 | 320 | |||
402 | 321 | sys.exit(not result.wasSuccessful()) | ||
403 | 266 | 322 | ||
404 | 267 | 323 | ||
405 | 268 | if __name__ == '__main__': | 324 | if __name__ == '__main__': |
406 | 269 | 325 | ||
407 | === modified file 'debian/changelog' | |||
408 | --- debian/changelog 2011-09-15 20:52:05 +0000 | |||
409 | +++ debian/changelog 2012-01-04 20:35:34 +0000 | |||
410 | @@ -1,3 +1,23 @@ | |||
411 | 1 | ubuntuone-dev-tools (2.99.1-0ubuntu1) precise; urgency=low | ||
412 | 2 | |||
413 | 3 | * New upstream release. | ||
414 | 4 | |||
415 | 5 | -- Rodney Dawes <rodney.dawes@ubuntu.com> Wed, 04 Jan 2012 13:12:03 -0500 | ||
416 | 6 | |||
417 | 7 | ubuntuone-dev-tools (2.99.0-0ubuntu1) precise; urgency=low | ||
418 | 8 | |||
419 | 9 | * New upstream release. | ||
420 | 10 | - Depends on dirspec now (LP: #888619) | ||
421 | 11 | - Pass options through to trial (LP: #890786) | ||
422 | 12 | * debian/control: | ||
423 | 13 | - Move python-gobject to Suggests | ||
424 | 14 | - Suggest python-qt4reactor | ||
425 | 15 | * debian/rules: | ||
426 | 16 | - Convert to pure dh | ||
427 | 17 | - Run unit tests during build | ||
428 | 18 | |||
429 | 19 | -- Rodney Dawes <rodney.dawes@ubuntu.com> Thu, 22 Dec 2011 16:33:22 -0500 | ||
430 | 20 | |||
431 | 1 | ubuntuone-dev-tools (0.2.0-0ubuntu1) oneiric; urgency=low | 21 | ubuntuone-dev-tools (0.2.0-0ubuntu1) oneiric; urgency=low |
432 | 2 | 22 | ||
433 | 3 | * New upstream release. | 23 | * New upstream release. |
434 | 4 | 24 | ||
435 | === modified file 'debian/control' | |||
436 | --- debian/control 2011-09-15 20:52:05 +0000 | |||
437 | +++ debian/control 2012-01-04 20:35:34 +0000 | |||
438 | @@ -3,27 +3,29 @@ | |||
439 | 3 | XSBC-Original-Maintainer: Rodney Dawes <rodney.dawes@ubuntu.com> | 3 | XSBC-Original-Maintainer: Rodney Dawes <rodney.dawes@ubuntu.com> |
440 | 4 | Section: python | 4 | Section: python |
441 | 5 | Priority: optional | 5 | Priority: optional |
443 | 6 | Standards-Version: 3.9.1 | 6 | Standards-Version: 3.9.2 |
444 | 7 | X-Python-Version: >= 2.6 | ||
445 | 7 | Build-Depends-Indep: | 8 | Build-Depends-Indep: |
446 | 8 | dbus, | 9 | dbus, |
447 | 9 | pep8, | 10 | pep8, |
448 | 10 | pylint (>= 0.21.0), | 11 | pylint (>= 0.21.0), |
449 | 11 | python-coverage, | 12 | python-coverage, |
450 | 12 | python-dbus, | 13 | python-dbus, |
451 | 14 | python-dirspec (>= 2.99.0), | ||
452 | 13 | python-gobject, | 15 | python-gobject, |
453 | 14 | python-qt4, | 16 | python-qt4, |
454 | 15 | python-setuptools, | 17 | python-setuptools, |
459 | 16 | python-support, | 18 | python-twisted-core |
460 | 17 | python-twisted-core, | 19 | Build-Depends: debhelper (>= 7.0.50), |
461 | 18 | python-xdg | 20 | python-all (>= 2.6.6-3) |
458 | 19 | Build-Depends: cdbs (>= 0.4.43), debhelper (>= 7.0.17), python-all | ||
462 | 20 | Homepage: http://launchpad.net/ubuntuone-dev-tools | 21 | Homepage: http://launchpad.net/ubuntuone-dev-tools |
463 | 21 | 22 | ||
464 | 22 | Package: python-ubuntuone-devtools | 23 | Package: python-ubuntuone-devtools |
465 | 23 | Architecture: all | 24 | Architecture: all |
466 | 24 | Depends: ${python:Depends}, ${misc:Depends}, | 25 | Depends: ${python:Depends}, ${misc:Depends}, |
467 | 25 | dbus, | 26 | dbus, |
469 | 26 | python-dbus | 27 | python-dbus, |
470 | 28 | python-dirspec (>= 2.99.0) | ||
471 | 27 | Description: Ubuntu One development tools - Python modules | 29 | Description: Ubuntu One development tools - Python modules |
472 | 28 | Ubuntu One development tools provides scripts, test cases, and other | 30 | Ubuntu One development tools provides scripts, test cases, and other |
473 | 29 | utilities for developing Python projects which need more integration | 31 | utilities for developing Python projects which need more integration |
474 | @@ -35,10 +37,9 @@ | |||
475 | 35 | Depends: ${python:Depends}, ${misc:Depends}, python, | 37 | Depends: ${python:Depends}, ${misc:Depends}, python, |
476 | 36 | pylint (>= 0.21.0) | pyflakes, | 38 | pylint (>= 0.21.0) | pyflakes, |
477 | 37 | python-coverage, | 39 | python-coverage, |
478 | 38 | python-gobject, | ||
479 | 39 | python-twisted-core, | 40 | python-twisted-core, |
482 | 40 | python-ubuntuone-devtools (= ${binary:Version}), | 41 | python-ubuntuone-devtools (= ${binary:Version}) |
483 | 41 | python-xdg | 42 | Suggests: python-gobject, python-qt4reactor |
484 | 42 | Description: Ubuntu One development tools | 43 | Description: Ubuntu One development tools |
485 | 43 | Ubuntu One development tools provides scripts, test cases, and other | 44 | Ubuntu One development tools provides scripts, test cases, and other |
486 | 44 | utilities for developing Python projects which need more integration | 45 | utilities for developing Python projects which need more integration |
487 | 45 | 46 | ||
488 | === removed file 'debian/pycompat' | |||
489 | --- debian/pycompat 2010-08-02 13:45:54 +0000 | |||
490 | +++ debian/pycompat 1970-01-01 00:00:00 +0000 | |||
491 | @@ -1,1 +0,0 @@ | |||
492 | 1 | 2 | ||
493 | 2 | 0 | ||
494 | === modified file 'debian/rules' | |||
495 | --- debian/rules 2010-11-30 18:59:50 +0000 | |||
496 | +++ debian/rules 2012-01-04 20:35:34 +0000 | |||
497 | @@ -1,13 +1,8 @@ | |||
498 | 1 | #!/usr/bin/make -f | 1 | #!/usr/bin/make -f |
499 | 2 | 2 | ||
511 | 3 | include /usr/share/cdbs/1/rules/debhelper.mk | 3 | %: |
512 | 4 | DEB_PYTHON_SYSTEM=pysupport | 4 | dh --with python2 $@ |
513 | 5 | include /usr/share/cdbs/1/class/python-distutils.mk | 5 | |
514 | 6 | 6 | override_dh_auto_test: | |
515 | 7 | common-build-arch common-build-indep:: debian/stamp-check | 7 | ./run-tests |
516 | 8 | debian/stamp-check: | 8 | |
506 | 9 | cd $(DEB_SRCDIR) && ./run-tests | ||
507 | 10 | touch $@ | ||
508 | 11 | |||
509 | 12 | makefile-clean:: | ||
510 | 13 | rm -f debian/stamp-check | ||
517 | 14 | 9 | ||
518 | === modified file 'run-tests' | |||
519 | --- run-tests 2011-09-13 14:48:46 +0000 | |||
520 | +++ run-tests 2012-01-04 20:35:34 +0000 | |||
521 | @@ -1,7 +1,7 @@ | |||
522 | 1 | #!/bin/bash | 1 | #!/bin/bash |
523 | 2 | # Author: Natalia Bidart <natalia.bidart@canonical.com> | 2 | # Author: Natalia Bidart <natalia.bidart@canonical.com> |
524 | 3 | # | 3 | # |
526 | 4 | # Copyright 2010 Canonical Ltd. | 4 | # Copyright 2010-2011 Canonical Ltd. |
527 | 5 | # | 5 | # |
528 | 6 | # This program is free software: you can redistribute it and/or modify it | 6 | # This program is free software: you can redistribute it and/or modify it |
529 | 7 | # under the terms of the GNU General Public License version 3, as published | 7 | # under the terms of the GNU General Public License version 3, as published |
530 | @@ -18,7 +18,8 @@ | |||
531 | 18 | 18 | ||
532 | 19 | bin/u1trial -c ubuntuone | 19 | bin/u1trial -c ubuntuone |
533 | 20 | bin/u1trial --reactor=twisted ubuntuone | 20 | bin/u1trial --reactor=twisted ubuntuone |
534 | 21 | echo "Running style checks..." | ||
535 | 21 | bin/u1lint | 22 | bin/u1lint |
537 | 22 | pep8 --repeat . | 23 | pep8 --repeat . bin/* |
538 | 23 | rm -rf _trial_temp | 24 | rm -rf _trial_temp |
539 | 24 | rm -rf .coverage | 25 | rm -rf .coverage |
540 | 25 | 26 | ||
541 | === modified file 'setup.py' | |||
542 | --- setup.py 2011-09-13 14:48:46 +0000 | |||
543 | +++ setup.py 2012-01-04 20:35:34 +0000 | |||
544 | @@ -21,7 +21,7 @@ | |||
545 | 21 | from distutils.core import setup, Command | 21 | from distutils.core import setup, Command |
546 | 22 | 22 | ||
547 | 23 | PACKAGE = 'ubuntuone-dev-tools' | 23 | PACKAGE = 'ubuntuone-dev-tools' |
549 | 24 | VERSION = '0.2.0' | 24 | VERSION = '2.99.1' |
550 | 25 | 25 | ||
551 | 26 | U1LINT = 'bin/u1lint' | 26 | U1LINT = 'bin/u1lint' |
552 | 27 | 27 | ||
553 | @@ -51,7 +51,9 @@ | |||
554 | 51 | packages=['ubuntuone', | 51 | packages=['ubuntuone', |
555 | 52 | 'ubuntuone.devtools', | 52 | 'ubuntuone.devtools', |
556 | 53 | 'ubuntuone.devtools.reactors', | 53 | 'ubuntuone.devtools.reactors', |
558 | 54 | 'ubuntuone.devtools.services'], | 54 | 'ubuntuone.devtools.services', |
559 | 55 | 'ubuntuone.devtools.testing', | ||
560 | 56 | 'ubuntuone.devtools.testcases'], | ||
561 | 55 | extra_path='ubuntuone-dev-tools', | 57 | extra_path='ubuntuone-dev-tools', |
562 | 56 | scripts=['bin/u1lint', | 58 | scripts=['bin/u1lint', |
563 | 57 | 'bin/u1trial', | 59 | 'bin/u1trial', |
564 | 58 | 60 | ||
565 | === modified file 'ubuntuone/devtools/reactors/glib.py' | |||
566 | --- ubuntuone/devtools/reactors/glib.py 2011-09-13 14:48:46 +0000 | |||
567 | +++ ubuntuone/devtools/reactors/glib.py 2012-01-04 20:35:34 +0000 | |||
568 | @@ -17,7 +17,7 @@ | |||
569 | 17 | def install(options=None): | 17 | def install(options=None): |
570 | 18 | """Install the reactor and parse any options we might need.""" | 18 | """Install the reactor and parse any options we might need.""" |
571 | 19 | reactor_name = None | 19 | reactor_name = None |
573 | 20 | if options is not None and options.use_gui: | 20 | if options is not None and options['gui']: |
574 | 21 | reactor_name = 'twisted.internet.gtk2reactor' | 21 | reactor_name = 'twisted.internet.gtk2reactor' |
575 | 22 | else: | 22 | else: |
576 | 23 | reactor_name = 'twisted.internet.glib2reactor' | 23 | reactor_name = 'twisted.internet.glib2reactor' |
577 | 24 | 24 | ||
578 | === modified file 'ubuntuone/devtools/reactors/qt4.py' | |||
579 | --- ubuntuone/devtools/reactors/qt4.py 2011-09-13 14:48:46 +0000 | |||
580 | +++ ubuntuone/devtools/reactors/qt4.py 2012-01-04 20:35:34 +0000 | |||
581 | @@ -22,12 +22,19 @@ | |||
582 | 22 | 22 | ||
583 | 23 | def install(options=None): | 23 | def install(options=None): |
584 | 24 | """Install the reactor and parse any options we might need.""" | 24 | """Install the reactor and parse any options we might need.""" |
586 | 25 | if options is not None and options.use_gui: | 25 | if options is not None and options['gui']: |
587 | 26 | from PyQt4.QtGui import QApplication | 26 | from PyQt4.QtGui import QApplication |
588 | 27 | # We must assign this to a variable, or we will get crashes in Qt | 27 | # We must assign this to a variable, or we will get crashes in Qt |
589 | 28 | # pylint: disable=W0612 | 28 | # pylint: disable=W0612 |
590 | 29 | app = QApplication(sys.argv) | 29 | app = QApplication(sys.argv) |
591 | 30 | # pylint: enable=W0612 | 30 | # pylint: enable=W0612 |
592 | 31 | 31 | ||
594 | 32 | qt4reactor = __import__('qtreactor.qt4reactor', None, None, ['']) | 32 | try: |
595 | 33 | qt4reactor = __import__('qt4reactor', None, None, ['']) | ||
596 | 34 | except ImportError: | ||
597 | 35 | # Leave this import in place for a couple of weeks | ||
598 | 36 | # until all the devs are using the packaged qt4reactor | ||
599 | 37 | # (nessita, 11-11-11) | ||
600 | 38 | qt4reactor = __import__('qtreactor.qt4reactor', None, None, ['']) | ||
601 | 39 | |||
602 | 33 | qt4reactor.install() | 40 | qt4reactor.install() |
603 | 34 | 41 | ||
604 | === modified file 'ubuntuone/devtools/services/dbus.py' | |||
605 | --- ubuntuone/devtools/services/dbus.py 2011-09-13 14:48:46 +0000 | |||
606 | +++ ubuntuone/devtools/services/dbus.py 2012-01-04 20:35:34 +0000 | |||
607 | @@ -20,9 +20,9 @@ | |||
608 | 20 | import signal | 20 | import signal |
609 | 21 | import subprocess | 21 | import subprocess |
610 | 22 | 22 | ||
611 | 23 | from dirspec.basedir import load_data_paths | ||
612 | 23 | from distutils.spawn import find_executable | 24 | from distutils.spawn import find_executable |
613 | 24 | from urllib import quote | 25 | from urllib import quote |
614 | 25 | from xdg.BaseDirectory import load_data_paths | ||
615 | 26 | 26 | ||
616 | 27 | 27 | ||
617 | 28 | class DBusLaunchError(Exception): | 28 | class DBusLaunchError(Exception): |
618 | 29 | 29 | ||
619 | === modified file 'ubuntuone/devtools/testcase.py' | |||
620 | --- ubuntuone/devtools/testcase.py 2011-09-13 14:48:46 +0000 | |||
621 | +++ ubuntuone/devtools/testcase.py 2012-01-04 20:35:34 +0000 | |||
622 | @@ -1,8 +1,6 @@ | |||
623 | 1 | # -*- coding: utf-8 -*- | 1 | # -*- coding: utf-8 -*- |
624 | 2 | |||
625 | 3 | # Author: Guillermo Gonzalez <guillermo.gonzalez@canonical.com> | ||
626 | 4 | # | 2 | # |
628 | 5 | # Copyright 2009-2010 Canonical Ltd. | 3 | # Copyright 2011 Canonical Ltd. |
629 | 6 | # | 4 | # |
630 | 7 | # This program is free software: you can redistribute it and/or modify it | 5 | # This program is free software: you can redistribute it and/or modify it |
631 | 8 | # under the terms of the GNU General Public License version 3, as published | 6 | # under the terms of the GNU General Public License version 3, as published |
632 | @@ -15,245 +13,13 @@ | |||
633 | 15 | # | 13 | # |
634 | 16 | # You should have received a copy of the GNU General Public License along | 14 | # You should have received a copy of the GNU General Public License along |
635 | 17 | # with this program. If not, see <http://www.gnu.org/licenses/>. | 15 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
878 | 18 | 16 | """Maintain old API.""" | |
879 | 19 | """Base tests cases and test utilities.""" | 17 | |
880 | 20 | 18 | import warnings | |
881 | 21 | from __future__ import with_statement | 19 | warnings.warn( |
882 | 22 | 20 | 'Deprecated import path; use ubuntuone.devtools.testcases' | |
883 | 23 | import contextlib | 21 | 'instead.', DeprecationWarning, stacklevel=2) |
884 | 24 | import os | 22 | |
885 | 25 | import shutil | 23 | # pylint: disable=W0401,W0614 |
886 | 26 | import sys | 24 | from ubuntuone.devtools.testcases import * |
887 | 27 | 25 | from ubuntuone.devtools.testcases.dbus import * | |
646 | 28 | from functools import wraps | ||
647 | 29 | |||
648 | 30 | from twisted.internet import defer | ||
649 | 31 | from twisted.trial.unittest import TestCase, SkipTest | ||
650 | 32 | |||
651 | 33 | # DBusRunner for DBusTestCase using tests | ||
652 | 34 | from ubuntuone.devtools.services.dbus import DBusRunner | ||
653 | 35 | |||
654 | 36 | |||
655 | 37 | # pylint: disable=F0401,C0103 | ||
656 | 38 | try: | ||
657 | 39 | import dbus | ||
658 | 40 | except ImportError: | ||
659 | 41 | dbus = None | ||
660 | 42 | |||
661 | 43 | try: | ||
662 | 44 | import dbus.service as service | ||
663 | 45 | except ImportError: | ||
664 | 46 | service = None | ||
665 | 47 | |||
666 | 48 | try: | ||
667 | 49 | from dbus.mainloop.glib import DBusGMainLoop | ||
668 | 50 | except ImportError: | ||
669 | 51 | DBusGMainLoop = None | ||
670 | 52 | |||
671 | 53 | |||
672 | 54 | # pylint: enable=F0401,C0103 | ||
673 | 55 | @contextlib.contextmanager | ||
674 | 56 | def environ(env_var, new_value): | ||
675 | 57 | """context manager to replace/add an environ value""" | ||
676 | 58 | old_value = os.environ.get(env_var, None) | ||
677 | 59 | os.environ[env_var] = new_value | ||
678 | 60 | yield | ||
679 | 61 | if old_value is None: | ||
680 | 62 | os.environ.pop(env_var) | ||
681 | 63 | else: | ||
682 | 64 | os.environ[env_var] = old_value | ||
683 | 65 | |||
684 | 66 | |||
685 | 67 | def _id(obj): | ||
686 | 68 | """Return the obj calling the funct.""" | ||
687 | 69 | return obj | ||
688 | 70 | |||
689 | 71 | |||
690 | 72 | # pylint: disable=C0103 | ||
691 | 73 | def skipTest(reason): | ||
692 | 74 | """Unconditionally skip a test.""" | ||
693 | 75 | |||
694 | 76 | def decorator(test_item): | ||
695 | 77 | """Decorate the test so that it is skipped.""" | ||
696 | 78 | if not (isinstance(test_item, type) and\ | ||
697 | 79 | issubclass(test_item, TestCase)): | ||
698 | 80 | |||
699 | 81 | @wraps(test_item) | ||
700 | 82 | def skip_wrapper(*args, **kwargs): | ||
701 | 83 | """Skip a test method raising an exception.""" | ||
702 | 84 | raise SkipTest(reason) | ||
703 | 85 | test_item = skip_wrapper | ||
704 | 86 | |||
705 | 87 | # tell twisted.trial.unittest to skip the test, pylint will complain | ||
706 | 88 | # since it thinks we are redefining a name out of the scope | ||
707 | 89 | # pylint: disable=W0621,W0612 | ||
708 | 90 | test_item.skip = reason | ||
709 | 91 | # pylint: enable=W0621,W0612 | ||
710 | 92 | # because the item was skipped, we will make sure that no | ||
711 | 93 | # services are started for it | ||
712 | 94 | if hasattr(test_item, "required_services"): | ||
713 | 95 | # pylint: disable=W0612 | ||
714 | 96 | test_item.required_services = lambda *args, **kwargs: [] | ||
715 | 97 | # pylint: enable=W0612 | ||
716 | 98 | return test_item | ||
717 | 99 | return decorator | ||
718 | 100 | |||
719 | 101 | |||
720 | 102 | def skipIf(condition, reason): | ||
721 | 103 | """Skip a test if the condition is true.""" | ||
722 | 104 | if condition: | ||
723 | 105 | return skipTest(reason) | ||
724 | 106 | return _id | ||
725 | 107 | |||
726 | 108 | |||
727 | 109 | def skipIfOS(current_os, reason): | ||
728 | 110 | """Skip test for a particular os or lists of them.""" | ||
729 | 111 | if os: | ||
730 | 112 | if sys.platform in current_os or sys.platform == current_os: | ||
731 | 113 | return skipTest(reason) | ||
732 | 114 | return _id | ||
733 | 115 | return _id | ||
734 | 116 | |||
735 | 117 | |||
736 | 118 | def skipIfNotOS(current_os, reason): | ||
737 | 119 | """Skip test we are not in a particular os.""" | ||
738 | 120 | if os: | ||
739 | 121 | if sys.platform not in current_os or\ | ||
740 | 122 | sys.platform != current_os: | ||
741 | 123 | return skipTest(reason) | ||
742 | 124 | return _id | ||
743 | 125 | return _id | ||
744 | 126 | |||
745 | 127 | |||
746 | 128 | # pylint: enable=C0103 | ||
747 | 129 | |||
748 | 130 | |||
749 | 131 | class InvalidSessionBus(Exception): | ||
750 | 132 | """Error when we are connected to the wrong session bus in tests.""" | ||
751 | 133 | |||
752 | 134 | |||
753 | 135 | class FakeDBusInterface(object): | ||
754 | 136 | """A fake DBusInterface...""" | ||
755 | 137 | |||
756 | 138 | def shutdown(self, with_restart=False): | ||
757 | 139 | """...that only knows how to go away""" | ||
758 | 140 | |||
759 | 141 | |||
760 | 142 | class BaseTestCase(TestCase): | ||
761 | 143 | """Base TestCase with helper methods to handle temp dir. | ||
762 | 144 | |||
763 | 145 | This class provides: | ||
764 | 146 | mktemp(name): helper to create temporary dirs | ||
765 | 147 | rmtree(path): support read-only shares | ||
766 | 148 | makedirs(path): support read-only shares | ||
767 | 149 | |||
768 | 150 | """ | ||
769 | 151 | |||
770 | 152 | def required_services(self): | ||
771 | 153 | """Return the list of required services for DBusTestCase.""" | ||
772 | 154 | return [] | ||
773 | 155 | |||
774 | 156 | def mktemp(self, name='temp'): | ||
775 | 157 | """Customized mktemp that accepts an optional name argument.""" | ||
776 | 158 | tempdir = os.path.join(self.tmpdir, name) | ||
777 | 159 | if os.path.exists(tempdir): | ||
778 | 160 | self.rmtree(tempdir) | ||
779 | 161 | self.makedirs(tempdir) | ||
780 | 162 | return tempdir | ||
781 | 163 | |||
782 | 164 | @property | ||
783 | 165 | def tmpdir(self): | ||
784 | 166 | """Default tmpdir: module/class/test_method.""" | ||
785 | 167 | # check if we already generated the root path | ||
786 | 168 | root_dir = getattr(self, '__root', None) | ||
787 | 169 | if root_dir: | ||
788 | 170 | return root_dir | ||
789 | 171 | max_filename = 32 # some platforms limit lengths of filenames | ||
790 | 172 | base = os.path.join(self.__class__.__module__[:max_filename], | ||
791 | 173 | self.__class__.__name__[:max_filename], | ||
792 | 174 | self._testMethodName[:max_filename]) | ||
793 | 175 | # use _trial_temp dir, it should be os.gwtcwd() | ||
794 | 176 | # define the root temp dir of the testcase, pylint: disable=W0201 | ||
795 | 177 | self.__root = os.path.join(os.getcwd(), base) | ||
796 | 178 | return self.__root | ||
797 | 179 | |||
798 | 180 | def rmtree(self, path): | ||
799 | 181 | """Custom rmtree that handle ro parent(s) and childs.""" | ||
800 | 182 | if not os.path.exists(path): | ||
801 | 183 | return | ||
802 | 184 | # change perms to rw, so we can delete the temp dir | ||
803 | 185 | if path != getattr(self, '__root', None): | ||
804 | 186 | os.chmod(os.path.dirname(path), 0755) | ||
805 | 187 | if not os.access(path, os.W_OK): | ||
806 | 188 | os.chmod(path, 0755) | ||
807 | 189 | # pylint: disable=W0612 | ||
808 | 190 | for dirpath, dirs, files in os.walk(path): | ||
809 | 191 | for dirname in dirs: | ||
810 | 192 | if not os.access(os.path.join(dirpath, dirname), os.W_OK): | ||
811 | 193 | os.chmod(os.path.join(dirpath, dirname), 0777) | ||
812 | 194 | shutil.rmtree(path) | ||
813 | 195 | |||
814 | 196 | def makedirs(self, path): | ||
815 | 197 | """Custom makedirs that handle ro parent.""" | ||
816 | 198 | parent = os.path.dirname(path) | ||
817 | 199 | if os.path.exists(parent): | ||
818 | 200 | os.chmod(parent, 0755) | ||
819 | 201 | os.makedirs(path) | ||
820 | 202 | |||
821 | 203 | |||
822 | 204 | @skipIf(dbus is None or service is None or DBusGMainLoop is None, | ||
823 | 205 | "The test requires dbus.") | ||
824 | 206 | class DBusTestCase(BaseTestCase): | ||
825 | 207 | """Test the DBus event handling.""" | ||
826 | 208 | |||
827 | 209 | def required_services(self): | ||
828 | 210 | """Return the list of required services for DBusTestCase.""" | ||
829 | 211 | services = super(DBusTestCase, self).required_services() | ||
830 | 212 | services.extend([DBusRunner]) | ||
831 | 213 | return services | ||
832 | 214 | |||
833 | 215 | @defer.inlineCallbacks | ||
834 | 216 | def setUp(self): | ||
835 | 217 | """Setup the infrastructure fo the test (dbus service).""" | ||
836 | 218 | # Class 'BaseTestCase' has no 'setUp' member | ||
837 | 219 | # pylint: disable=E1101 | ||
838 | 220 | # dbus modules will be imported by the decorator | ||
839 | 221 | # pylint: disable=E0602 | ||
840 | 222 | yield super(DBusTestCase, self).setUp() | ||
841 | 223 | |||
842 | 224 | # We need to ensure DBUS_SESSION_BUS_ADDRESS is private here | ||
843 | 225 | from urllib import unquote | ||
844 | 226 | bus_address = os.environ.get('DBUS_SESSION_BUS_ADDRESS', None) | ||
845 | 227 | if os.path.dirname(unquote(bus_address.split(',')[0].split('=')[1])) \ | ||
846 | 228 | != os.path.dirname(os.getcwd()): | ||
847 | 229 | raise InvalidSessionBus('DBUS_SESSION_BUS_ADDRES is wrong.') | ||
848 | 230 | |||
849 | 231 | # Set up the main loop and bus connection | ||
850 | 232 | self.loop = DBusGMainLoop(set_as_default=True) | ||
851 | 233 | self.bus = dbus.bus.BusConnection(address_or_type=bus_address, | ||
852 | 234 | mainloop=self.loop) | ||
853 | 235 | |||
854 | 236 | # Monkeypatch the dbus.SessionBus/SystemBus methods, to ensure we | ||
855 | 237 | # always point at our own private bus instance. | ||
856 | 238 | self.patch(dbus, 'SessionBus', lambda: self.bus) | ||
857 | 239 | self.patch(dbus, 'SystemBus', lambda: self.bus) | ||
858 | 240 | |||
859 | 241 | # Check that we are on the correct bus for real | ||
860 | 242 | # Disable this for now, because our tests are extremely broken :( | ||
861 | 243 | # bus_names = self.bus.list_names() | ||
862 | 244 | # if len(bus_names) > 2: | ||
863 | 245 | # raise InvalidSessionBus('Too many bus connections: %s (%r)' % | ||
864 | 246 | # (len(bus_names), bus_names)) | ||
865 | 247 | |||
866 | 248 | # monkeypatch busName.__del__ to avoid errors on gc | ||
867 | 249 | # we take care of releasing the name in shutdown | ||
868 | 250 | service.BusName.__del__ = lambda _: None | ||
869 | 251 | yield self.bus.set_exit_on_disconnect(False) | ||
870 | 252 | self.signal_receivers = set() | ||
871 | 253 | |||
872 | 254 | @defer.inlineCallbacks | ||
873 | 255 | def tearDown(self): | ||
874 | 256 | """Cleanup the test.""" | ||
875 | 257 | yield self.bus.flush() | ||
876 | 258 | yield self.bus.close() | ||
877 | 259 | yield super(DBusTestCase, self).tearDown() | ||
888 | 260 | 26 | ||
889 | === added directory 'ubuntuone/devtools/testcases' | |||
890 | === added file 'ubuntuone/devtools/testcases/__init__.py' | |||
891 | --- ubuntuone/devtools/testcases/__init__.py 1970-01-01 00:00:00 +0000 | |||
892 | +++ ubuntuone/devtools/testcases/__init__.py 2012-01-04 20:35:34 +0000 | |||
893 | @@ -0,0 +1,166 @@ | |||
894 | 1 | # -*- coding: utf-8 -*- | ||
895 | 2 | # | ||
896 | 3 | # Copyright 2009-2011 Canonical Ltd. | ||
897 | 4 | # | ||
898 | 5 | # This program is free software: you can redistribute it and/or modify it | ||
899 | 6 | # under the terms of the GNU General Public License version 3, as published | ||
900 | 7 | # by the Free Software Foundation. | ||
901 | 8 | # | ||
902 | 9 | # This program is distributed in the hope that it will be useful, but | ||
903 | 10 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
904 | 11 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
905 | 12 | # PURPOSE. See the GNU General Public License for more details. | ||
906 | 13 | # | ||
907 | 14 | # You should have received a copy of the GNU General Public License along | ||
908 | 15 | # with this program. If not, see <http://www.gnu.org/licenses/>. | ||
909 | 16 | |||
910 | 17 | """Base tests cases and test utilities.""" | ||
911 | 18 | |||
912 | 19 | from __future__ import with_statement | ||
913 | 20 | |||
914 | 21 | import contextlib | ||
915 | 22 | import os | ||
916 | 23 | import shutil | ||
917 | 24 | import sys | ||
918 | 25 | |||
919 | 26 | from functools import wraps | ||
920 | 27 | |||
921 | 28 | from twisted.trial.unittest import TestCase, SkipTest | ||
922 | 29 | |||
923 | 30 | |||
924 | 31 | @contextlib.contextmanager | ||
925 | 32 | def environ(env_var, new_value): | ||
926 | 33 | """context manager to replace/add an environ value""" | ||
927 | 34 | old_value = os.environ.get(env_var, None) | ||
928 | 35 | os.environ[env_var] = new_value | ||
929 | 36 | yield | ||
930 | 37 | if old_value is None: | ||
931 | 38 | os.environ.pop(env_var) | ||
932 | 39 | else: | ||
933 | 40 | os.environ[env_var] = old_value | ||
934 | 41 | |||
935 | 42 | |||
936 | 43 | def _id(obj): | ||
937 | 44 | """Return the obj calling the funct.""" | ||
938 | 45 | return obj | ||
939 | 46 | |||
940 | 47 | |||
941 | 48 | # pylint: disable=C0103 | ||
942 | 49 | def skipTest(reason): | ||
943 | 50 | """Unconditionally skip a test.""" | ||
944 | 51 | |||
945 | 52 | def decorator(test_item): | ||
946 | 53 | """Decorate the test so that it is skipped.""" | ||
947 | 54 | if not (isinstance(test_item, type) and\ | ||
948 | 55 | issubclass(test_item, TestCase)): | ||
949 | 56 | |||
950 | 57 | @wraps(test_item) | ||
951 | 58 | def skip_wrapper(*args, **kwargs): | ||
952 | 59 | """Skip a test method raising an exception.""" | ||
953 | 60 | raise SkipTest(reason) | ||
954 | 61 | test_item = skip_wrapper | ||
955 | 62 | |||
956 | 63 | # tell twisted.trial.unittest to skip the test, pylint will complain | ||
957 | 64 | # since it thinks we are redefining a name out of the scope | ||
958 | 65 | # pylint: disable=W0621,W0612 | ||
959 | 66 | test_item.skip = reason | ||
960 | 67 | # pylint: enable=W0621,W0612 | ||
961 | 68 | # because the item was skipped, we will make sure that no | ||
962 | 69 | # services are started for it | ||
963 | 70 | if hasattr(test_item, "required_services"): | ||
964 | 71 | # pylint: disable=W0612 | ||
965 | 72 | test_item.required_services = lambda *args, **kwargs: [] | ||
966 | 73 | # pylint: enable=W0612 | ||
967 | 74 | return test_item | ||
968 | 75 | return decorator | ||
969 | 76 | |||
970 | 77 | |||
971 | 78 | def skipIf(condition, reason): | ||
972 | 79 | """Skip a test if the condition is true.""" | ||
973 | 80 | if condition: | ||
974 | 81 | return skipTest(reason) | ||
975 | 82 | return _id | ||
976 | 83 | |||
977 | 84 | |||
978 | 85 | def skipIfOS(current_os, reason): | ||
979 | 86 | """Skip test for a particular os or lists of them.""" | ||
980 | 87 | if os: | ||
981 | 88 | if sys.platform in current_os or sys.platform == current_os: | ||
982 | 89 | return skipTest(reason) | ||
983 | 90 | return _id | ||
984 | 91 | return _id | ||
985 | 92 | |||
986 | 93 | |||
987 | 94 | def skipIfNotOS(current_os, reason): | ||
988 | 95 | """Skip test we are not in a particular os.""" | ||
989 | 96 | if os: | ||
990 | 97 | if sys.platform not in current_os or\ | ||
991 | 98 | sys.platform != current_os: | ||
992 | 99 | return skipTest(reason) | ||
993 | 100 | return _id | ||
994 | 101 | return _id | ||
995 | 102 | |||
996 | 103 | |||
997 | 104 | # pylint: enable=C0103 | ||
998 | 105 | |||
999 | 106 | |||
1000 | 107 | class BaseTestCase(TestCase): | ||
1001 | 108 | """Base TestCase with helper methods to handle temp dir. | ||
1002 | 109 | |||
1003 | 110 | This class provides: | ||
1004 | 111 | mktemp(name): helper to create temporary dirs | ||
1005 | 112 | rmtree(path): support read-only shares | ||
1006 | 113 | makedirs(path): support read-only shares | ||
1007 | 114 | |||
1008 | 115 | """ | ||
1009 | 116 | |||
1010 | 117 | def required_services(self): | ||
1011 | 118 | """Return the list of required services for DBusTestCase.""" | ||
1012 | 119 | return [] | ||
1013 | 120 | |||
1014 | 121 | def mktemp(self, name='temp'): | ||
1015 | 122 | """Customized mktemp that accepts an optional name argument.""" | ||
1016 | 123 | tempdir = os.path.join(self.tmpdir, name) | ||
1017 | 124 | if os.path.exists(tempdir): | ||
1018 | 125 | self.rmtree(tempdir) | ||
1019 | 126 | self.makedirs(tempdir) | ||
1020 | 127 | return tempdir | ||
1021 | 128 | |||
1022 | 129 | @property | ||
1023 | 130 | def tmpdir(self): | ||
1024 | 131 | """Default tmpdir: module/class/test_method.""" | ||
1025 | 132 | # check if we already generated the root path | ||
1026 | 133 | root_dir = getattr(self, '__root', None) | ||
1027 | 134 | if root_dir: | ||
1028 | 135 | return root_dir | ||
1029 | 136 | max_filename = 32 # some platforms limit lengths of filenames | ||
1030 | 137 | base = os.path.join(self.__class__.__module__[:max_filename], | ||
1031 | 138 | self.__class__.__name__[:max_filename], | ||
1032 | 139 | self._testMethodName[:max_filename]) | ||
1033 | 140 | # use _trial_temp dir, it should be os.gwtcwd() | ||
1034 | 141 | # define the root temp dir of the testcase, pylint: disable=W0201 | ||
1035 | 142 | self.__root = os.path.join(os.getcwd(), base) | ||
1036 | 143 | return self.__root | ||
1037 | 144 | |||
1038 | 145 | def rmtree(self, path): | ||
1039 | 146 | """Custom rmtree that handle ro parent(s) and childs.""" | ||
1040 | 147 | if not os.path.exists(path): | ||
1041 | 148 | return | ||
1042 | 149 | # change perms to rw, so we can delete the temp dir | ||
1043 | 150 | if path != getattr(self, '__root', None): | ||
1044 | 151 | os.chmod(os.path.dirname(path), 0755) | ||
1045 | 152 | if not os.access(path, os.W_OK): | ||
1046 | 153 | os.chmod(path, 0755) | ||
1047 | 154 | # pylint: disable=W0612 | ||
1048 | 155 | for dirpath, dirs, files in os.walk(path): | ||
1049 | 156 | for dirname in dirs: | ||
1050 | 157 | if not os.access(os.path.join(dirpath, dirname), os.W_OK): | ||
1051 | 158 | os.chmod(os.path.join(dirpath, dirname), 0777) | ||
1052 | 159 | shutil.rmtree(path) | ||
1053 | 160 | |||
1054 | 161 | def makedirs(self, path): | ||
1055 | 162 | """Custom makedirs that handle ro parent.""" | ||
1056 | 163 | parent = os.path.dirname(path) | ||
1057 | 164 | if os.path.exists(parent): | ||
1058 | 165 | os.chmod(parent, 0755) | ||
1059 | 166 | os.makedirs(path) | ||
1060 | 0 | 167 | ||
1061 | === added file 'ubuntuone/devtools/testcases/dbus.py' | |||
1062 | --- ubuntuone/devtools/testcases/dbus.py 1970-01-01 00:00:00 +0000 | |||
1063 | +++ ubuntuone/devtools/testcases/dbus.py 2012-01-04 20:35:34 +0000 | |||
1064 | @@ -0,0 +1,118 @@ | |||
1065 | 1 | # -*- coding: utf-8 -*- | ||
1066 | 2 | # | ||
1067 | 3 | # Copyright 2009-2011 Canonical Ltd. | ||
1068 | 4 | # | ||
1069 | 5 | # This program is free software: you can redistribute it and/or modify it | ||
1070 | 6 | # under the terms of the GNU General Public License version 3, as published | ||
1071 | 7 | # by the Free Software Foundation. | ||
1072 | 8 | # | ||
1073 | 9 | # This program is distributed in the hope that it will be useful, but | ||
1074 | 10 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
1075 | 11 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
1076 | 12 | # PURPOSE. See the GNU General Public License for more details. | ||
1077 | 13 | # | ||
1078 | 14 | # You should have received a copy of the GNU General Public License along | ||
1079 | 15 | # with this program. If not, see <http://www.gnu.org/licenses/>. | ||
1080 | 16 | |||
1081 | 17 | """Base tests cases and test utilities.""" | ||
1082 | 18 | |||
1083 | 19 | from __future__ import absolute_import, with_statement | ||
1084 | 20 | |||
1085 | 21 | import os | ||
1086 | 22 | |||
1087 | 23 | from twisted.internet import defer | ||
1088 | 24 | |||
1089 | 25 | # lint seems not tow work well when we use decorators | ||
1090 | 26 | # pylint:disable=W0611 | ||
1091 | 27 | from ubuntuone.devtools.testcase import BaseTestCase, skipIf | ||
1092 | 28 | # pylint:enable=W0611 | ||
1093 | 29 | # DBusRunner for DBusTestCase using tests | ||
1094 | 30 | from ubuntuone.devtools.services.dbus import DBusRunner | ||
1095 | 31 | |||
1096 | 32 | |||
1097 | 33 | # pylint: disable=F0401,C0103,W0406,E0611 | ||
1098 | 34 | try: | ||
1099 | 35 | import dbus | ||
1100 | 36 | except ImportError, e: | ||
1101 | 37 | dbus = None | ||
1102 | 38 | |||
1103 | 39 | try: | ||
1104 | 40 | import dbus.service as service | ||
1105 | 41 | except ImportError: | ||
1106 | 42 | service = None | ||
1107 | 43 | |||
1108 | 44 | try: | ||
1109 | 45 | from dbus.mainloop.glib import DBusGMainLoop | ||
1110 | 46 | except ImportError: | ||
1111 | 47 | DBusGMainLoop = None | ||
1112 | 48 | |||
1113 | 49 | # pylint: enable=F0401,C0103,W0406,E0611 | ||
1114 | 50 | |||
1115 | 51 | |||
1116 | 52 | class InvalidSessionBus(Exception): | ||
1117 | 53 | """Error when we are connected to the wrong session bus in tests.""" | ||
1118 | 54 | |||
1119 | 55 | |||
1120 | 56 | class FakeDBusInterface(object): | ||
1121 | 57 | """A fake DBusInterface...""" | ||
1122 | 58 | |||
1123 | 59 | def shutdown(self, with_restart=False): | ||
1124 | 60 | """...that only knows how to go away""" | ||
1125 | 61 | |||
1126 | 62 | |||
1127 | 63 | @skipIf(dbus is None or service is None or DBusGMainLoop is None, | ||
1128 | 64 | "The test requires dbus.") | ||
1129 | 65 | class DBusTestCase(BaseTestCase): | ||
1130 | 66 | """Test the DBus event handling.""" | ||
1131 | 67 | |||
1132 | 68 | def required_services(self): | ||
1133 | 69 | """Return the list of required services for DBusTestCase.""" | ||
1134 | 70 | services = super(DBusTestCase, self).required_services() | ||
1135 | 71 | services.extend([DBusRunner]) | ||
1136 | 72 | return services | ||
1137 | 73 | |||
1138 | 74 | @defer.inlineCallbacks | ||
1139 | 75 | def setUp(self): | ||
1140 | 76 | """Setup the infrastructure fo the test (dbus service).""" | ||
1141 | 77 | # Class 'BaseTestCase' has no 'setUp' member | ||
1142 | 78 | # pylint: disable=E1101 | ||
1143 | 79 | # dbus modules will be imported by the decorator | ||
1144 | 80 | # pylint: disable=E0602 | ||
1145 | 81 | yield super(DBusTestCase, self).setUp() | ||
1146 | 82 | |||
1147 | 83 | # We need to ensure DBUS_SESSION_BUS_ADDRESS is private here | ||
1148 | 84 | from urllib import unquote | ||
1149 | 85 | bus_address = os.environ.get('DBUS_SESSION_BUS_ADDRESS', None) | ||
1150 | 86 | if os.path.dirname(unquote(bus_address.split(',')[0].split('=')[1])) \ | ||
1151 | 87 | != os.path.dirname(os.getcwd()): | ||
1152 | 88 | raise InvalidSessionBus('DBUS_SESSION_BUS_ADDRES is wrong.') | ||
1153 | 89 | |||
1154 | 90 | # Set up the main loop and bus connection | ||
1155 | 91 | self.loop = DBusGMainLoop(set_as_default=True) | ||
1156 | 92 | self.bus = dbus.bus.BusConnection(address_or_type=bus_address, | ||
1157 | 93 | mainloop=self.loop) | ||
1158 | 94 | |||
1159 | 95 | # Monkeypatch the dbus.SessionBus/SystemBus methods, to ensure we | ||
1160 | 96 | # always point at our own private bus instance. | ||
1161 | 97 | self.patch(dbus, 'SessionBus', lambda: self.bus) | ||
1162 | 98 | self.patch(dbus, 'SystemBus', lambda: self.bus) | ||
1163 | 99 | |||
1164 | 100 | # Check that we are on the correct bus for real | ||
1165 | 101 | # Disable this for now, because our tests are extremely broken :( | ||
1166 | 102 | # bus_names = self.bus.list_names() | ||
1167 | 103 | # if len(bus_names) > 2: | ||
1168 | 104 | # raise InvalidSessionBus('Too many bus connections: %s (%r)' % | ||
1169 | 105 | # (len(bus_names), bus_names)) | ||
1170 | 106 | |||
1171 | 107 | # monkeypatch busName.__del__ to avoid errors on gc | ||
1172 | 108 | # we take care of releasing the name in shutdown | ||
1173 | 109 | service.BusName.__del__ = lambda _: None | ||
1174 | 110 | yield self.bus.set_exit_on_disconnect(False) | ||
1175 | 111 | self.signal_receivers = set() | ||
1176 | 112 | |||
1177 | 113 | @defer.inlineCallbacks | ||
1178 | 114 | def tearDown(self): | ||
1179 | 115 | """Cleanup the test.""" | ||
1180 | 116 | yield self.bus.flush() | ||
1181 | 117 | yield self.bus.close() | ||
1182 | 118 | yield super(DBusTestCase, self).tearDown() | ||
1183 | 0 | 119 | ||
1184 | === added directory 'ubuntuone/devtools/testing' | |||
1185 | === added file 'ubuntuone/devtools/testing/__init__.py' | |||
1186 | --- ubuntuone/devtools/testing/__init__.py 1970-01-01 00:00:00 +0000 | |||
1187 | +++ ubuntuone/devtools/testing/__init__.py 2012-01-04 20:35:34 +0000 | |||
1188 | @@ -0,0 +1,1 @@ | |||
1189 | 1 | """Testing helpers.""" | ||
1190 | 0 | 2 | ||
1191 | === added file 'ubuntuone/devtools/testing/txcheck.py' | |||
1192 | --- ubuntuone/devtools/testing/txcheck.py 1970-01-01 00:00:00 +0000 | |||
1193 | +++ ubuntuone/devtools/testing/txcheck.py 2012-01-04 20:35:34 +0000 | |||
1194 | @@ -0,0 +1,366 @@ | |||
1195 | 1 | # -*- coding: utf-8 -*- | ||
1196 | 2 | |||
1197 | 3 | # Author: Tim Cole <tim.cole@canonical.com> | ||
1198 | 4 | # | ||
1199 | 5 | # Copyright 2011 Canonical Ltd. | ||
1200 | 6 | # | ||
1201 | 7 | # This program is free software: you can redistribute it and/or modify it | ||
1202 | 8 | # under the terms of the GNU General Public License version 3, as published | ||
1203 | 9 | # by the Free Software Foundation. | ||
1204 | 10 | # | ||
1205 | 11 | # This program is distributed in the hope that it will be useful, but | ||
1206 | 12 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
1207 | 13 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
1208 | 14 | # PURPOSE. See the GNU General Public License for more details. | ||
1209 | 15 | # | ||
1210 | 16 | # You should have received a copy of the GNU General Public License along | ||
1211 | 17 | # with this program. If not, see <http://www.gnu.org/licenses/>. | ||
1212 | 18 | |||
1213 | 19 | """Utilities for performing correctness checks.""" | ||
1214 | 20 | |||
1215 | 21 | import sys | ||
1216 | 22 | import ast | ||
1217 | 23 | from inspect import getsource | ||
1218 | 24 | from textwrap import dedent | ||
1219 | 25 | from itertools import takewhile | ||
1220 | 26 | from unittest import TestCase, TestSuite, TestResult | ||
1221 | 27 | |||
1222 | 28 | from twisted.trial.unittest import TestCase as TwistedTestCase | ||
1223 | 29 | |||
1224 | 30 | |||
1225 | 31 | def type_to_name(type_obj): | ||
1226 | 32 | """Return a name for a type.""" | ||
1227 | 33 | package_name = getattr(type_obj, '__module__', None) | ||
1228 | 34 | if package_name: | ||
1229 | 35 | return "%s.%s" % (package_name, type_obj.__name__) | ||
1230 | 36 | else: | ||
1231 | 37 | return type_obj.__name__ | ||
1232 | 38 | |||
1233 | 39 | |||
1234 | 40 | class Problem(AssertionError): | ||
1235 | 41 | """An object representing a problem in a method.""" | ||
1236 | 42 | |||
1237 | 43 | def __init__(self, method, test_class, ancestor_class): | ||
1238 | 44 | """Initialize an instance.""" | ||
1239 | 45 | super(Problem, self).__init__() | ||
1240 | 46 | self.method = method | ||
1241 | 47 | self.test_class = test_class | ||
1242 | 48 | self.ancestor_class = ancestor_class | ||
1243 | 49 | |||
1244 | 50 | def __eq__(self, other): | ||
1245 | 51 | """Test equality.""" | ||
1246 | 52 | return type(self) == type(other) and self.__dict__ == other.__dict__ | ||
1247 | 53 | |||
1248 | 54 | def __ne__(self, other): | ||
1249 | 55 | """Test inequality.""" | ||
1250 | 56 | return not (self == other) | ||
1251 | 57 | |||
1252 | 58 | def __hash__(self): | ||
1253 | 59 | """Return hash.""" | ||
1254 | 60 | member_hash = 0 | ||
1255 | 61 | for (key, value) in self.__dict__.iteritems(): | ||
1256 | 62 | member_hash ^= hash(key) ^ hash(value) | ||
1257 | 63 | return hash(type(self)) ^ member_hash | ||
1258 | 64 | |||
1259 | 65 | def __str__(self): | ||
1260 | 66 | """Return a friendlier representation.""" | ||
1261 | 67 | if self.ancestor_class != self.test_class: | ||
1262 | 68 | method_string = ("%s in ancestor method %s.%s" % | ||
1263 | 69 | (type_to_name(self.test_class), | ||
1264 | 70 | type_to_name(self.ancestor_class), | ||
1265 | 71 | self.method)) | ||
1266 | 72 | else: | ||
1267 | 73 | method_string = ("%s.%s" % | ||
1268 | 74 | (type_to_name(self.test_class), self.method)) | ||
1269 | 75 | return ("%s for %s" % (type(self).__name__, method_string)) | ||
1270 | 76 | |||
1271 | 77 | def __repr__(self): | ||
1272 | 78 | """Return representation string.""" | ||
1273 | 79 | return "<%s %r>" % (type(self), self.__dict__) | ||
1274 | 80 | |||
1275 | 81 | |||
1276 | 82 | class MethodShadowed(Problem): | ||
1277 | 83 | """Problem when trial's run method is shadowed.""" | ||
1278 | 84 | |||
1279 | 85 | |||
1280 | 86 | class SuperResultDiscarded(Problem): | ||
1281 | 87 | """Problem when callback chains are broken.""" | ||
1282 | 88 | |||
1283 | 89 | |||
1284 | 90 | class SuperNotCalled(Problem): | ||
1285 | 91 | """Problem when super isn't called.""" | ||
1286 | 92 | |||
1287 | 93 | |||
1288 | 94 | class MissingInlineCallbacks(Problem): | ||
1289 | 95 | """Problem when the inlineCallbacks decorator is missing.""" | ||
1290 | 96 | |||
1291 | 97 | |||
1292 | 98 | class MissingReturnValue(Problem): | ||
1293 | 99 | """Problem when there's no return value.""" | ||
1294 | 100 | |||
1295 | 101 | |||
1296 | 102 | def match_type(expected_type): | ||
1297 | 103 | """Return predicate matching nodes of given type.""" | ||
1298 | 104 | return lambda node: isinstance(node, expected_type) | ||
1299 | 105 | |||
1300 | 106 | |||
1301 | 107 | def match_equal(expected_value): | ||
1302 | 108 | """Return predicate matching nodes equaling the given value.""" | ||
1303 | 109 | return lambda node: expected_value == node | ||
1304 | 110 | |||
1305 | 111 | |||
1306 | 112 | def match_in(expected_values): | ||
1307 | 113 | """Return predicate matching node if in collection of expected values.""" | ||
1308 | 114 | return lambda node: node in expected_values | ||
1309 | 115 | |||
1310 | 116 | |||
1311 | 117 | def match_not_none(): | ||
1312 | 118 | """Returns a predicate matching nodes which are not None.""" | ||
1313 | 119 | return lambda node: node is not None | ||
1314 | 120 | |||
1315 | 121 | |||
1316 | 122 | def match_any(*subtests): | ||
1317 | 123 | """Return short-circuiting predicate matching any given subpredicate.""" | ||
1318 | 124 | if len(subtests) == 1: | ||
1319 | 125 | return subtests[0] | ||
1320 | 126 | else: | ||
1321 | 127 | |||
1322 | 128 | def test(node): | ||
1323 | 129 | """Try each subtest until we find one that passes.""" | ||
1324 | 130 | for subtest in subtests: | ||
1325 | 131 | if subtest(node): | ||
1326 | 132 | return True | ||
1327 | 133 | return False | ||
1328 | 134 | |||
1329 | 135 | return test | ||
1330 | 136 | |||
1331 | 137 | |||
1332 | 138 | def match_all(*subtests): | ||
1333 | 139 | """Return short-circuiting predicate matching all given subpredicates.""" | ||
1334 | 140 | if len(subtests) == 1: | ||
1335 | 141 | return subtests[0] | ||
1336 | 142 | else: | ||
1337 | 143 | |||
1338 | 144 | def test(node): | ||
1339 | 145 | """Try each subtest until we find one that fails.""" | ||
1340 | 146 | for subtest in subtests: | ||
1341 | 147 | if not subtest(node): | ||
1342 | 148 | return False | ||
1343 | 149 | return True | ||
1344 | 150 | |||
1345 | 151 | return test | ||
1346 | 152 | |||
1347 | 153 | |||
1348 | 154 | def match_attr(attr_name, *tests): | ||
1349 | 155 | """Return predicate matching subpredicates against an attribute value.""" | ||
1350 | 156 | return lambda node: match_all(*tests)(getattr(node, attr_name)) | ||
1351 | 157 | |||
1352 | 158 | |||
1353 | 159 | def match_path(initial_test, *components): | ||
1354 | 160 | """Return predicate which recurses into the tree via given attributes.""" | ||
1355 | 161 | components = list(components) | ||
1356 | 162 | components.reverse() | ||
1357 | 163 | test = lambda node: True | ||
1358 | 164 | for component in components: | ||
1359 | 165 | attr_name = component[0] | ||
1360 | 166 | subtests = component[1:] | ||
1361 | 167 | test = match_attr(attr_name, match_all(match_all(*subtests), test)) | ||
1362 | 168 | return match_all(initial_test, test) | ||
1363 | 169 | |||
1364 | 170 | |||
1365 | 171 | def match_child(*tests): | ||
1366 | 172 | """Return predicate which tests any child.""" | ||
1367 | 173 | subtest = match_all(*tests) | ||
1368 | 174 | |||
1369 | 175 | def test(node): | ||
1370 | 176 | """Try each child until we find one that matches.""" | ||
1371 | 177 | for child in ast.iter_child_nodes(node): | ||
1372 | 178 | if subtest(child): | ||
1373 | 179 | return True | ||
1374 | 180 | return False | ||
1375 | 181 | |||
1376 | 182 | return test | ||
1377 | 183 | |||
1378 | 184 | |||
1379 | 185 | def match_descendant(subtest, prune): | ||
1380 | 186 | """Return predicate which tests a node and any descendants.""" | ||
1381 | 187 | |||
1382 | 188 | def test(node): | ||
1383 | 189 | """Recursively (breadth-first) search for a matching node.""" | ||
1384 | 190 | for child in ast.iter_child_nodes(node): | ||
1385 | 191 | if prune(child): | ||
1386 | 192 | continue | ||
1387 | 193 | if subtest(child) or test(child): | ||
1388 | 194 | return True | ||
1389 | 195 | return False | ||
1390 | 196 | |||
1391 | 197 | return test | ||
1392 | 198 | |||
1393 | 199 | |||
1394 | 200 | def matches(node, *tests): | ||
1395 | 201 | """Convenience function to try predicates on a node.""" | ||
1396 | 202 | return match_all(*tests)(node) | ||
1397 | 203 | |||
1398 | 204 | |||
1399 | 205 | def any_matches(nodes, *tests): | ||
1400 | 206 | """Convenience function to try predicates on any of a sequence of nodes.""" | ||
1401 | 207 | test = match_all(*tests) | ||
1402 | 208 | for node in nodes: | ||
1403 | 209 | if test(node): | ||
1404 | 210 | return True | ||
1405 | 211 | return False | ||
1406 | 212 | |||
1407 | 213 | |||
1408 | 214 | def iter_matching_child_nodes(node, *tests): | ||
1409 | 215 | """Yields every matching child node.""" | ||
1410 | 216 | test = match_all(*tests) | ||
1411 | 217 | for child in ast.iter_child_nodes(node): | ||
1412 | 218 | if test(child): | ||
1413 | 219 | yield child | ||
1414 | 220 | |||
1415 | 221 | |||
1416 | 222 | SETUP_FUNCTION_NAMES = ('setUp', 'tearDown') | ||
1417 | 223 | SETUP_FUNCTION = match_path(match_type(ast.FunctionDef), | ||
1418 | 224 | ('name', match_in(SETUP_FUNCTION_NAMES))) | ||
1419 | 225 | |||
1420 | 226 | SUPER = match_path(match_type(ast.Call), | ||
1421 | 227 | ('func', match_type(ast.Attribute)), | ||
1422 | 228 | ('value', match_type(ast.Call)), | ||
1423 | 229 | ('func', match_type(ast.Name)), | ||
1424 | 230 | ('id', match_equal("super"))) | ||
1425 | 231 | |||
1426 | 232 | BARE_SUPER = match_path(match_type(ast.Expr), | ||
1427 | 233 | ('value', SUPER)) | ||
1428 | 234 | |||
1429 | 235 | YIELD = match_type(ast.Yield) | ||
1430 | 236 | |||
1431 | 237 | INLINE_CALLBACKS_DECORATOR = \ | ||
1432 | 238 | match_any(match_path(match_type(ast.Attribute), | ||
1433 | 239 | ('attr', match_equal('inlineCallbacks'))), | ||
1434 | 240 | match_path(match_type(ast.Name), | ||
1435 | 241 | ('id', match_equal('inlineCallbacks')))) | ||
1436 | 242 | |||
1437 | 243 | RETURN_VALUE = \ | ||
1438 | 244 | match_path(match_type(ast.Return), | ||
1439 | 245 | ('value', match_not_none())) | ||
1440 | 246 | |||
1441 | 247 | DEFS = match_any(match_type(ast.ClassDef), | ||
1442 | 248 | match_type(ast.FunctionDef)) | ||
1443 | 249 | |||
1444 | 250 | |||
1445 | 251 | def find_problems(class_to_check): | ||
1446 | 252 | """Check twisted test setup in a given test class.""" | ||
1447 | 253 | mro = class_to_check.__mro__ | ||
1448 | 254 | if TwistedTestCase not in mro: | ||
1449 | 255 | return set() | ||
1450 | 256 | |||
1451 | 257 | problems = set() | ||
1452 | 258 | |||
1453 | 259 | ancestry = takewhile(lambda c: c != TwistedTestCase, mro) | ||
1454 | 260 | for ancestor_class in ancestry: | ||
1455 | 261 | if 'run' in ancestor_class.__dict__: | ||
1456 | 262 | problem = MethodShadowed(method='run', | ||
1457 | 263 | test_class=class_to_check, | ||
1458 | 264 | ancestor_class=ancestor_class) | ||
1459 | 265 | problems.add(problem) | ||
1460 | 266 | |||
1461 | 267 | source = dedent(getsource(ancestor_class)) | ||
1462 | 268 | tree = ast.parse(source) | ||
1463 | 269 | # the top level of the tree is a Module | ||
1464 | 270 | class_node = tree.body[0] | ||
1465 | 271 | |||
1466 | 272 | # Check setUp/tearDown | ||
1467 | 273 | for def_node in iter_matching_child_nodes(class_node, SETUP_FUNCTION): | ||
1468 | 274 | if matches(def_node, match_child(BARE_SUPER)): | ||
1469 | 275 | # Superclass method called, but its result wasn't used | ||
1470 | 276 | problem = SuperResultDiscarded(method=def_node.name, | ||
1471 | 277 | test_class=class_to_check, | ||
1472 | 278 | ancestor_class=ancestor_class) | ||
1473 | 279 | problems.add(problem) | ||
1474 | 280 | if not matches(def_node, match_descendant(SUPER, DEFS)): | ||
1475 | 281 | # The call to the overridden superclass method is missing | ||
1476 | 282 | problem = SuperNotCalled(method=def_node.name, | ||
1477 | 283 | test_class=class_to_check, | ||
1478 | 284 | ancestor_class=ancestor_class) | ||
1479 | 285 | problems.add(problem) | ||
1480 | 286 | |||
1481 | 287 | decorators = def_node.decorator_list | ||
1482 | 288 | |||
1483 | 289 | if matches(def_node, match_descendant(YIELD, DEFS)): | ||
1484 | 290 | # Yield was used, making this a generator | ||
1485 | 291 | if not any_matches(decorators, INLINE_CALLBACKS_DECORATOR): | ||
1486 | 292 | # ...but the inlineCallbacks decorator is missing | ||
1487 | 293 | problem = MissingInlineCallbacks( | ||
1488 | 294 | method=def_node.name, | ||
1489 | 295 | test_class=class_to_check, | ||
1490 | 296 | ancestor_class=ancestor_class) | ||
1491 | 297 | problems.add(problem) | ||
1492 | 298 | else: | ||
1493 | 299 | if not matches(def_node, match_descendant(RETURN_VALUE, DEFS)): | ||
1494 | 300 | # The function fails to return a deferred | ||
1495 | 301 | problem = MissingReturnValue( | ||
1496 | 302 | method=def_node.name, | ||
1497 | 303 | test_class=class_to_check, | ||
1498 | 304 | ancestor_class=ancestor_class) | ||
1499 | 305 | problems.add(problem) | ||
1500 | 306 | |||
1501 | 307 | return problems | ||
1502 | 308 | |||
1503 | 309 | |||
1504 | 310 | def get_test_classes(suite): | ||
1505 | 311 | """Return all the unique test classes involved in a suite.""" | ||
1506 | 312 | classes = set() | ||
1507 | 313 | |||
1508 | 314 | def find_classes(suite_or_test): | ||
1509 | 315 | """Recursively find all the test classes.""" | ||
1510 | 316 | if isinstance(suite_or_test, TestSuite): | ||
1511 | 317 | for subtest in suite_or_test: | ||
1512 | 318 | find_classes(subtest) | ||
1513 | 319 | else: | ||
1514 | 320 | classes.add(type(suite_or_test)) | ||
1515 | 321 | |||
1516 | 322 | find_classes(suite) | ||
1517 | 323 | |||
1518 | 324 | return classes | ||
1519 | 325 | |||
1520 | 326 | |||
1521 | 327 | def make_check_testcase(tests): | ||
1522 | 328 | """Make TestCase which checks the given twisted tests.""" | ||
1523 | 329 | |||
1524 | 330 | class TXCheckTest(TestCase): | ||
1525 | 331 | """Test case which checks the test classes for problems.""" | ||
1526 | 332 | |||
1527 | 333 | def runTest(self): # pylint: disable=C0103 | ||
1528 | 334 | """Do nothing.""" | ||
1529 | 335 | |||
1530 | 336 | def run(self, result=None): | ||
1531 | 337 | """Check all the test classes for problems.""" | ||
1532 | 338 | if result is None: | ||
1533 | 339 | result = TestResult() | ||
1534 | 340 | |||
1535 | 341 | test_classes = set() | ||
1536 | 342 | |||
1537 | 343 | for test_object in tests: | ||
1538 | 344 | test_classes |= get_test_classes(test_object) | ||
1539 | 345 | |||
1540 | 346 | for test_class in test_classes: | ||
1541 | 347 | problems = find_problems(test_class) | ||
1542 | 348 | for problem in problems: | ||
1543 | 349 | try: | ||
1544 | 350 | raise problem | ||
1545 | 351 | except Problem: | ||
1546 | 352 | result.addFailure(self, sys.exc_info()) | ||
1547 | 353 | |||
1548 | 354 | return TXCheckTest() | ||
1549 | 355 | |||
1550 | 356 | |||
1551 | 357 | class TXCheckSuite(TestSuite): | ||
1552 | 358 | """Test suite which checks twisted tests.""" | ||
1553 | 359 | |||
1554 | 360 | def __init__(self, tests=()): | ||
1555 | 361 | """Initialize with the given tests, and add a special test.""" | ||
1556 | 362 | |||
1557 | 363 | tests = list(tests) | ||
1558 | 364 | tests.insert(0, make_check_testcase(self)) | ||
1559 | 365 | |||
1560 | 366 | super(TXCheckSuite, self).__init__(tests) |
Needs bug 907888 to be resolved first. changelog entry has wrong email address, but that not a big deal.