Merge lp:~dobey/ubuntuone-dev-tools/update-4-0 into lp:ubuntuone-dev-tools/stable-4-0

Proposed by dobey
Status: Merged
Approved by: Roberto Alsina
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
Reviewer Review Type Date Requested Status
Roberto Alsina (community) Approve
Review via email: mp+120457@code.launchpad.net

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

To post a comment you must log in.
Revision history for this message
Roberto Alsina (ralsina) :
review: Approve
79. By dobey

[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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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- print
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)

Subscribers

People subscribed via source and target branches

to all changes: