Merge lp:~dobey/ubuntuone-dev-tools/runner-refactor into lp:ubuntuone-dev-tools

Proposed by dobey
Status: Merged
Approved by: Alejandro J. Cura
Approved revision: 82
Merged at revision: 77
Proposed branch: lp:~dobey/ubuntuone-dev-tools/runner-refactor
Merge into: lp:ubuntuone-dev-tools
Diff against target: 993 lines (+619/-305)
8 files modified
bin/u1trial (+2/-303)
run-tests (+1/-1)
setup.py (+1/-0)
ubuntuone/devtools/errors.py (+35/-0)
ubuntuone/devtools/runners/__init__.py (+286/-0)
ubuntuone/devtools/runners/txrunner.py (+126/-0)
ubuntuone/devtools/testcases/dbus.py (+1/-1)
ubuntuone/devtools/utils.py (+167/-0)
To merge this branch: bzr merge lp:~dobey/ubuntuone-dev-tools/runner-refactor
Reviewer Review Type Date Requested Status
Alejandro J. Cura (community) Approve
Roberto Alsina (community) Approve
Review via email: mp+117122@code.launchpad.net

Commit message

Refactor u1trial to allow multiple different test running back-ends
Implement Option handling API that is compatible with twisted trial
Remove logic from u1trial script
Fix a typo in the dbus test case
Ignore .pc directory for pep8 in run-tests

To post a comment you must log in.
Revision history for this message
Roberto Alsina (ralsina) :
review: Approve
Revision history for this message
Alejandro J. Cura (alecu) wrote :

_get_flags and _get_params are too much alike, I think the common code should be merged.

There are no tests for the new code. Please create a new bug for that.

The code looks fine otherwise.

review: Needs Fixing
82. By dobey

Consolidate the _get_flags and _get_params code into a single function

Revision history for this message
dobey (dobey) wrote :

Have merged the flag/params parsers into a single function which the other two call now.

As for testing, I haven't filed a bug yet, as I had just realized something about the testing and wanted to discuss first. We can probably add more tests, but as this code is required to work for u1trial to run, and we're running the dev-tools tests with the u1trial which is in the branch itself, the code has at least 90% coverage, if not 100%, simply by running the tests already. I suppose we could make it >100% by adding a bunch of fake objects and options lists, and ensuring somehow that the parsed output is what we'd expect, but also not sure how useful that is right now.

Revision history for this message
dobey (dobey) wrote :

Bug about needing more tests is now filed as bug #1032336.

Revision history for this message
Alejandro J. Cura (alecu) wrote :

Refactored code looks good.|

review: Approve

Preview Diff

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

Subscribers

People subscribed via source and target branches

to all changes: