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