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
=== modified file 'bin/u1trial'
--- bin/u1trial 2012-06-05 21:33:33 +0000
+++ bin/u1trial 2012-08-02 15:49:22 +0000
@@ -26,315 +26,14 @@
26# do not wish to do so, delete this exception statement from your26# do not wish to do so, delete this exception statement from your
27# version. If you delete this exception statement from all source27# version. If you delete this exception statement from all source
28# files in the program, then also delete it here.28# files in the program, then also delete it here.
29"""Test runner that uses a private dbus session and glib main loop."""29"""Test runner which works with special services and main loops."""
3030
31import coverage
32import gc
33import inspect
34import os31import os
35import re
36import sys32import sys
37import unittest
38
39from twisted.python.usage import UsageError
40from twisted.scripts import trial
41from twisted.trial.runner import TrialRunner
4233
43sys.path.insert(0, os.path.abspath("."))34sys.path.insert(0, os.path.abspath("."))
4435
45from ubuntuone.devtools.testing.txcheck import TXCheckSuite36from ubuntuone.devtools.runners import main
46
47
48def _is_in_ignored_path(testcase, paths):
49 """Return if the testcase is in one of the ignored paths."""
50 for ignored_path in paths:
51 if testcase.startswith(ignored_path):
52 return True
53 return False
54
55
56class TestRunner(TrialRunner):
57 """The test runner implementation."""
58
59 def __init__(self, config=None):
60 # set $HOME to the _trial_temp dir, to avoid breaking user files
61 trial_temp_dir = os.environ.get('TRIAL_TEMP_DIR', os.getcwd())
62 homedir = os.path.join(trial_temp_dir, config['temp-directory'])
63 os.environ['HOME'] = homedir
64
65 # setup $XDG_*_HOME variables and create the directories
66 xdg_cache = os.path.join(homedir, 'xdg_cache')
67 xdg_config = os.path.join(homedir, 'xdg_config')
68 xdg_data = os.path.join(homedir, 'xdg_data')
69 os.environ['XDG_CACHE_HOME'] = xdg_cache
70 os.environ['XDG_CONFIG_HOME'] = xdg_config
71 os.environ['XDG_DATA_HOME'] = xdg_data
72
73 if not os.path.exists(xdg_cache):
74 os.makedirs(xdg_cache)
75 if not os.path.exists(xdg_config):
76 os.makedirs(xdg_config)
77 if not os.path.exists(xdg_data):
78 os.makedirs(xdg_data)
79
80 # setup the ROOTDIR env var
81 os.environ['ROOTDIR'] = os.getcwd()
82
83 # Need an attribute for tempdir so we can use it later
84 self.tempdir = homedir
85 working_dir = os.path.join(self.tempdir, 'trial')
86
87 # Handle running trial in debug or dry-run mode
88 mode = None
89 if config['debug']:
90 mode = TrialRunner.DEBUG
91 if config['dry-run']:
92 mode = TrialRunner.DRY_RUN
93
94 # Hook up to the parent test runner
95 super(TestRunner, self).__init__(
96 reporterFactory=config['reporter'],
97 mode=mode,
98 profile=config['profile'],
99 logfile=config['logfile'],
100 tracebackFormat=config['tbformat'],
101 realTimeErrors=config['rterrors'],
102 uncleanWarnings=config['unclean-warnings'],
103 workingDirectory=working_dir,
104 forceGarbageCollection=config['force-gc'])
105
106 self.required_services = []
107 self.source_files = []
108
109 def _load_unittest(self, relpath):
110 """Load unit tests from a Python module with the given 'relpath'."""
111 assert relpath.endswith(".py"), (
112 "%s does not appear to be a Python module" % relpath)
113 if not os.path.basename(relpath).startswith('test_'):
114 return
115 modpath = relpath.replace(os.path.sep, ".")[:-3]
116 module = __import__(modpath, None, None, [""])
117
118 # If the module specifies required_services, make sure we get them
119 members = [x[1] for x in inspect.getmembers(module, inspect.isclass)]
120 for member_type in members:
121 if hasattr(member_type, 'required_services'):
122 member = member_type()
123 for service in member.required_services():
124 if service not in self.required_services:
125 self.required_services.append(service)
126 del member
127 gc.collect()
128
129 # If the module has a 'suite' or 'test_suite' function, use that
130 # to load the tests.
131 if hasattr(module, "suite"):
132 return module.suite()
133 elif hasattr(module, "test_suite"):
134 return module.test_suite()
135 else:
136 return unittest.defaultTestLoader.loadTestsFromModule(module)
137
138 def _collect_tests(self, path, test_pattern, ignored_modules,
139 ignored_paths):
140 """Return the set of unittests."""
141 suite = TXCheckSuite()
142 if test_pattern:
143 pattern = re.compile('.*%s.*' % test_pattern)
144 else:
145 pattern = None
146
147 # Disable this lint warning as we need to access _tests in the
148 # test suites, to collect the tests
149 # pylint: disable=W0212
150 if path:
151 try:
152 module_suite = self._load_unittest(path)
153 if pattern:
154 for inner_suite in module_suite._tests:
155 for test in inner_suite._tests:
156 if pattern.match(test.id()):
157 suite.addTest(test)
158 else:
159 suite.addTests(module_suite)
160 return suite
161 except AssertionError:
162 pass
163 else:
164 print 'Path should be defined.'
165 exit(1)
166
167 # We don't use the dirs variable, so ignore the warning
168 # pylint: disable=W0612
169 for root, dirs, files in os.walk(path):
170 for test in files:
171 filepath = os.path.join(root, test)
172 if test.endswith(".py") and test not in ignored_modules and \
173 not _is_in_ignored_path(filepath, ignored_paths):
174 self.source_files.append(filepath)
175 if test.startswith("test_"):
176 module_suite = self._load_unittest(filepath)
177 if pattern:
178 for inner_suite in module_suite._tests:
179 for test in inner_suite._tests:
180 if pattern.match(test.id()):
181 suite.addTest(test)
182 else:
183 suite.addTests(module_suite)
184 return suite
185
186 def get_suite(self, config):
187 """Get the test suite to use."""
188 suite = unittest.TestSuite()
189 for path in config['tests']:
190 suite.addTest(self._collect_tests(path, config['test'],
191 config['ignore-modules'],
192 config['ignore-paths']))
193 if config['loop']:
194 old_suite = suite
195 suite = unittest.TestSuite()
196 for _ in xrange(config['loop']):
197 suite.addTest(old_suite)
198
199 return suite
200
201 # pylint: disable=C0103
202 def _runWithoutDecoration(self, test):
203 """run the tests."""
204 result = None
205 running_services = []
206
207 try:
208 # Start any required services
209 for service in self.required_services:
210 runner = service()
211 runner.start_service(tempdir=self.tempdir)
212 running_services.append(runner)
213
214 result = super(TestRunner, self)._runWithoutDecoration(test)
215 finally:
216 # Stop all the running services
217 for runner in running_services:
218 runner.stop_service()
219
220 return result
221
222
223def _get_default_reactor():
224 """Return the platform-dependent default reactor to use."""
225 default_reactor = 'gi'
226 if sys.platform in ['darwin', 'win32']:
227 default_reactor = 'twisted'
228 return default_reactor
229
230
231class Options(trial.Options):
232 """Class for options handling."""
233
234 optFlags = [["coverage", "c"],
235 ["gui", None],
236 ["help-reactors", None],
237 ]
238
239 optParameters = [["test", "t", None],
240 ["loop", None, 1],
241 ["ignore-modules", "i", ""],
242 ["ignore-paths", "p", ""],
243 ["reactor", "r", _get_default_reactor()],
244 ]
245
246 def __init__(self):
247 self['tests'] = set()
248 super(Options, self).__init__()
249 self['rterrors'] = True
250
251 def opt_coverage(self):
252 """Generate a coverage report for the run tests"""
253 self['coverage'] = True
254
255 def opt_gui(self):
256 """Use the GUI mode of some reactors"""
257 self['gui'] = True
258
259 def opt_help_reactors(self):
260 """Help on available reactors for use with tests"""
261 synopsis = ('')
262 print synopsis
263 print 'Need to get list of reactors and print them here.'
264 print
265 sys.exit(0)
266
267 def opt_test(self, option):
268 """Run specific tests, e.g: className.methodName"""
269 self['test'] = option
270
271 def opt_loop(self, option):
272 """Loop tests the specified number of times."""
273 try:
274 self['loop'] = long(option)
275 except ValueError:
276 raise UsageError('A positive integer value must be specified.')
277
278 def opt_ignore_modules(self, option):
279 """Comma-separate list of test modules to ignore,
280 e.g: test_gtk.py, test_account.py
281 """
282 self['ignore-modules'] = map(str.strip, option.split(','))
283
284 def opt_ignore_paths(self, option):
285 """Comma-separated list of relative paths to ignore,
286 e.g: tests/platform/windows, tests/platform/macosx
287 """
288 self['ignore-paths'] = map(str.strip, option.split(','))
289
290 def opt_reactor(self, option):
291 """Which reactor to use (see --help-reactors for a list
292 of possibilities)
293 """
294 self['reactor'] = option
295 opt_r = opt_reactor
296
297
298def main():
299 """Do the deed."""
300 if len(sys.argv) == 1:
301 sys.argv.append('--help')
302
303 config = Options()
304 config.parseOptions()
305
306 try:
307 reactor_name = 'ubuntuone.devtools.reactors.%s' % config['reactor']
308 reactor = __import__(reactor_name, None, None, [''])
309 except ImportError:
310 print 'The specified reactor is not supported.'
311 sys.exit(1)
312 else:
313 try:
314 reactor.install(options=config)
315 except ImportError:
316 print('The Python package providing the requested reactor is not '
317 'installed. You can find it here: %s' % reactor.REACTOR_URL)
318 raise
319
320 trial_runner = TestRunner(config=config)
321 suite = trial_runner.get_suite(config)
322
323 if config['coverage']:
324 coverage.erase()
325 coverage.start()
326
327 if config['until-failure']:
328 result = trial_runner.runUntilFailure(suite)
329 else:
330 result = trial_runner.run(suite)
331
332 if config['coverage']:
333 coverage.stop()
334 coverage.report(trial_runner.source_files, ignore_errors=True,
335 show_missing=False)
336
337 sys.exit(not result.wasSuccessful())
33837
33938
340if __name__ == '__main__':39if __name__ == '__main__':
34140
=== modified file 'run-tests'
--- run-tests 2012-06-06 13:26:02 +0000
+++ run-tests 2012-08-02 15:49:22 +0000
@@ -21,6 +21,6 @@
21$PYTHON bin/u1trial --reactor=twisted -i "test_squid_windows.py" ubuntuone21$PYTHON bin/u1trial --reactor=twisted -i "test_squid_windows.py" ubuntuone
22echo "Running style checks..."22echo "Running style checks..."
23$PYTHON bin/u1lint23$PYTHON bin/u1lint
24pep8 --repeat . bin/* --exclude=*.bat24pep8 --repeat . bin/* --exclude=*.bat,.pc
25rm -rf _trial_temp25rm -rf _trial_temp
26rm -rf .coverage26rm -rf .coverage
2727
=== modified file 'setup.py'
--- setup.py 2012-06-05 21:28:52 +0000
+++ setup.py 2012-08-02 15:49:22 +0000
@@ -89,6 +89,7 @@
89 packages=['ubuntuone',89 packages=['ubuntuone',
90 'ubuntuone.devtools',90 'ubuntuone.devtools',
91 'ubuntuone.devtools.reactors',91 'ubuntuone.devtools.reactors',
92 'ubuntuone.devtools.runners',
92 'ubuntuone.devtools.services',93 'ubuntuone.devtools.services',
93 'ubuntuone.devtools.testing',94 'ubuntuone.devtools.testing',
94 'ubuntuone.devtools.testcases'],95 'ubuntuone.devtools.testcases'],
9596
=== added file 'ubuntuone/devtools/errors.py'
--- ubuntuone/devtools/errors.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/errors.py 2012-08-02 15:49:22 +0000
@@ -0,0 +1,35 @@
1# Copyright 2012 Canonical Ltd.
2#
3# This program is free software: you can redistribute it and/or modify it
4# under the terms of the GNU General Public License version 3, as published
5# by the Free Software Foundation.
6#
7# This program is distributed in the hope that it will be useful, but
8# WITHOUT ANY WARRANTY; without even the implied warranties of
9# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
10# PURPOSE. See the GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License along
13# with this program. If not, see <http://www.gnu.org/licenses/>.
14#
15# In addition, as a special exception, the copyright holders give
16# permission to link the code of portions of this program with the
17# OpenSSL library under certain conditions as described in each
18# individual source file, and distribute linked combinations
19# including the two.
20# You must obey the GNU General Public License in all respects
21# for all of the code used other than OpenSSL. If you modify
22# file(s) with this exception, you may extend this exception to your
23# version of the file(s), but you are not obligated to do so. If you
24# do not wish to do so, delete this exception statement from your
25# version. If you delete this exception statement from all source
26# files in the program, then also delete it here.
27"""Custom error types for Ubuntu One developer tools."""
28
29
30class TestError(Exception):
31 """An error occurred in attempting to load or start the tests."""
32
33
34class UsageError(Exception):
35 """An error occurred in parsing the command line arguments."""
036
=== added directory 'ubuntuone/devtools/runners'
=== added file 'ubuntuone/devtools/runners/__init__.py'
--- ubuntuone/devtools/runners/__init__.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/runners/__init__.py 2012-08-02 15:49:22 +0000
@@ -0,0 +1,286 @@
1# Copyright 2009-2012 Canonical Ltd.
2#
3# This program is free software: you can redistribute it and/or modify it
4# under the terms of the GNU General Public License version 3, as published
5# by the Free Software Foundation.
6#
7# This program is distributed in the hope that it will be useful, but
8# WITHOUT ANY WARRANTY; without even the implied warranties of
9# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
10# PURPOSE. See the GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License along
13# with this program. If not, see <http://www.gnu.org/licenses/>.
14#
15# In addition, as a special exception, the copyright holders give
16# permission to link the code of portions of this program with the
17# OpenSSL library under certain conditions as described in each
18# individual source file, and distribute linked combinations
19# including the two.
20# You must obey the GNU General Public License in all respects
21# for all of the code used other than OpenSSL. If you modify
22# file(s) with this exception, you may extend this exception to your
23# version of the file(s), but you are not obligated to do so. If you
24# do not wish to do so, delete this exception statement from your
25# version. If you delete this exception statement from all source
26# files in the program, then also delete it here.
27"""The base test runner object."""
28
29import coverage
30import gc
31import inspect
32import os
33import re
34import sys
35import unittest
36
37from ubuntuone.devtools.errors import TestError, UsageError
38from ubuntuone.devtools.testing.txcheck import TXCheckSuite
39from ubuntuone.devtools.utils import OptionParser
40
41__all__ = ['BaseTestOptions', 'BaseTestRunner', 'main']
42
43
44def _is_in_ignored_path(testcase, paths):
45 """Return if the testcase is in one of the ignored paths."""
46 for ignored_path in paths:
47 if testcase.startswith(ignored_path):
48 return True
49 return False
50
51
52class BaseTestRunner(object):
53 """The base test runner type. Does not actually run tests."""
54
55 def __init__(self, options=None, *args, **kwargs):
56 super(BaseTestRunner, self).__init__(*args, **kwargs)
57
58 # set $HOME to the _trial_temp dir, to avoid breaking user files
59 trial_temp_dir = os.environ.get('TRIAL_TEMP_DIR', os.getcwd())
60 homedir = os.path.join(trial_temp_dir, options['temp-directory'])
61 os.environ['HOME'] = homedir
62
63 # setup $XDG_*_HOME variables and create the directories
64 xdg_cache = os.path.join(homedir, 'xdg_cache')
65 xdg_config = os.path.join(homedir, 'xdg_config')
66 xdg_data = os.path.join(homedir, 'xdg_data')
67 os.environ['XDG_CACHE_HOME'] = xdg_cache
68 os.environ['XDG_CONFIG_HOME'] = xdg_config
69 os.environ['XDG_DATA_HOME'] = xdg_data
70
71 if not os.path.exists(xdg_cache):
72 os.makedirs(xdg_cache)
73 if not os.path.exists(xdg_config):
74 os.makedirs(xdg_config)
75 if not os.path.exists(xdg_data):
76 os.makedirs(xdg_data)
77
78 # setup the ROOTDIR env var
79 os.environ['ROOTDIR'] = os.getcwd()
80
81 # Need an attribute for tempdir so we can use it later
82 self.tempdir = homedir
83 self.working_dir = os.path.join(self.tempdir, 'trial')
84
85 self.source_files = []
86 self.required_services = []
87
88 def _load_unittest(self, relpath):
89 """Load unit tests from a Python module with the given 'relpath'."""
90 assert relpath.endswith(".py"), (
91 "%s does not appear to be a Python module" % relpath)
92 if not os.path.basename(relpath).startswith('test_'):
93 return
94 modpath = relpath.replace(os.path.sep, ".")[:-3]
95 module = __import__(modpath, None, None, [""])
96
97 # If the module specifies required_services, make sure we get them
98 members = [x[1] for x in inspect.getmembers(module, inspect.isclass)]
99 for member_type in members:
100 if hasattr(member_type, 'required_services'):
101 member = member_type()
102 for service in member.required_services():
103 if service not in self.required_services:
104 self.required_services.append(service)
105 del member
106 gc.collect()
107
108 # If the module has a 'suite' or 'test_suite' function, use that
109 # to load the tests.
110 if hasattr(module, "suite"):
111 return module.suite()
112 elif hasattr(module, "test_suite"):
113 return module.test_suite()
114 else:
115 return unittest.defaultTestLoader.loadTestsFromModule(module)
116
117 def _collect_tests(self, path, test_pattern, ignored_modules,
118 ignored_paths):
119 """Return the set of unittests."""
120 suite = TXCheckSuite()
121 if test_pattern:
122 pattern = re.compile('.*%s.*' % test_pattern)
123 else:
124 pattern = None
125
126 # Disable this lint warning as we need to access _tests in the
127 # test suites, to collect the tests
128 # pylint: disable=W0212
129 if path:
130 try:
131 module_suite = self._load_unittest(path)
132 if pattern:
133 for inner_suite in module_suite._tests:
134 for test in inner_suite._tests:
135 if pattern.match(test.id()):
136 suite.addTest(test)
137 else:
138 suite.addTests(module_suite)
139 return suite
140 except AssertionError:
141 pass
142 else:
143 raise TestError('Path should be defined.')
144
145 # We don't use the dirs variable, so ignore the warning
146 # pylint: disable=W0612
147 for root, dirs, files in os.walk(path):
148 for test in files:
149 filepath = os.path.join(root, test)
150 if test.endswith(".py") and test not in ignored_modules and \
151 not _is_in_ignored_path(filepath, ignored_paths):
152 self.source_files.append(filepath)
153 if test.startswith("test_"):
154 module_suite = self._load_unittest(filepath)
155 if pattern:
156 for inner_suite in module_suite._tests:
157 for test in inner_suite._tests:
158 if pattern.match(test.id()):
159 suite.addTest(test)
160 else:
161 suite.addTests(module_suite)
162 return suite
163
164 def get_suite(self, config):
165 """Get the test suite to use."""
166 suite = unittest.TestSuite()
167 for path in config['tests']:
168 suite.addTest(self._collect_tests(path, config['test'],
169 config['ignore-modules'],
170 config['ignore-paths']))
171 if config['loop']:
172 old_suite = suite
173 suite = unittest.TestSuite()
174 for _ in xrange(config['loop']):
175 suite.addTest(old_suite)
176
177 return suite
178
179 def run_tests(self, suite):
180 """Run the test suite."""
181 return False
182
183
184class BaseTestOptions(OptionParser):
185 """Base options for our test runner."""
186
187 optFlags = [['coverage', 'c', 'Generate a coverage report for the tests.'],
188 ['gui', None, 'Use the GUI mode of some runners.'],
189 ['help', 'h', ''],
190 ['help-runners', None, 'List information about test runners.'],
191 ]
192
193 optParameters = [['test', 't', None, None],
194 ['loop', None, 1, None],
195 ['ignore-modules', 'i', '', None],
196 ['ignore-paths', 'p', '', None],
197 ['runner', None, 'txrunner', None],
198 ]
199
200 def __init__(self, *args, **kwargs):
201 self['tests'] = set()
202 super(BaseTestOptions, self).__init__(*args, **kwargs)
203
204 def opt_help_runners(self):
205 """List the runners which are supported."""
206
207 def opt_ignore_modules(self, option):
208 """Comma-separate list of test modules to ignore,
209 e.g: test_gtk.py, test_account.py
210 """
211 self['ignore-modules'] = map(str.strip, option.split(','))
212
213 def opt_ignore_paths(self, option):
214 """Comma-separated list of relative paths to ignore,
215 e.g: tests/platform/windows, tests/platform/macosx
216 """
217 self['ignore-paths'] = map(str.strip, option.split(','))
218
219 def opt_loop(self, option):
220 """Loop tests the specified number of times."""
221 try:
222 self['loop'] = long(option)
223 except ValueError:
224 raise UsageError('A positive integer value must be specified.')
225
226 def opt_test(self, option):
227 """Run specific tests, e.g: className.methodName"""
228 self['test'] = option
229
230
231def _get_runner_options(runner_name):
232 """Return the test runner module, and its options object."""
233 module_name = 'ubuntuone.devtools.runners.%s' % runner_name
234 runner = __import__(module_name, None, None, [''])
235 options = None
236 if getattr(runner, 'TestOptions', None) is not None:
237 options = runner.TestOptions()
238 if options is None:
239 options = BaseTestOptions()
240 return (runner, options)
241
242
243def main():
244 """Do the deed."""
245 if len(sys.argv) == 1:
246 sys.argv.append('--help')
247
248 try:
249 pos = sys.argv.index('--runner')
250 runner_name = sys.argv.pop(pos + 1)
251 sys.argv.pop(pos)
252 except ValueError:
253 runner_name = 'txrunner'
254 finally:
255 runner, options = _get_runner_options(runner_name)
256 options.parseOptions()
257
258 test_runner = runner.TestRunner(options=options)
259 suite = test_runner.get_suite(options)
260
261 if options['coverage']:
262 coverage.erase()
263 coverage.start()
264
265 running_services = []
266
267 succeeded = False
268 try:
269 # Start any required services
270 for service_obj in test_runner.required_services:
271 service = service_obj()
272 service.start_service(tempdir=test_runner.tempdir)
273 running_services.append(service)
274
275 succeeded = test_runner.run_tests(suite)
276 finally:
277 # Stop all the running services
278 for service in running_services:
279 service.stop_service()
280
281 if options['coverage']:
282 coverage.stop()
283 coverage.report(test_runner.source_files, ignore_errors=True,
284 show_missing=False)
285
286 sys.exit(not succeeded)
0287
=== added file 'ubuntuone/devtools/runners/txrunner.py'
--- ubuntuone/devtools/runners/txrunner.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/runners/txrunner.py 2012-08-02 15:49:22 +0000
@@ -0,0 +1,126 @@
1# Copyright 2009-2012 Canonical Ltd.
2#
3# This program is free software: you can redistribute it and/or modify it
4# under the terms of the GNU General Public License version 3, as published
5# by the Free Software Foundation.
6#
7# This program is distributed in the hope that it will be useful, but
8# WITHOUT ANY WARRANTY; without even the implied warranties of
9# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
10# PURPOSE. See the GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License along
13# with this program. If not, see <http://www.gnu.org/licenses/>.
14#
15# In addition, as a special exception, the copyright holders give
16# permission to link the code of portions of this program with the
17# OpenSSL library under certain conditions as described in each
18# individual source file, and distribute linked combinations
19# including the two.
20# You must obey the GNU General Public License in all respects
21# for all of the code used other than OpenSSL. If you modify
22# file(s) with this exception, you may extend this exception to your
23# version of the file(s), but you are not obligated to do so. If you
24# do not wish to do so, delete this exception statement from your
25# version. If you delete this exception statement from all source
26# files in the program, then also delete it here.
27"""The twisted test runner and options."""
28import sys
29
30from twisted.scripts import trial
31from twisted.trial.runner import TrialRunner
32
33from ubuntuone.devtools.errors import TestError
34from ubuntuone.devtools.runners import BaseTestOptions, BaseTestRunner
35
36__all__ = ['TestRunner', 'TestOptions']
37
38
39class TestRunner(BaseTestRunner, TrialRunner):
40 """The twisted test runner implementation."""
41
42 def __init__(self, options=None):
43 # Handle running trial in debug or dry-run mode
44 self.config = options
45
46 try:
47 reactor_name = ('ubuntuone.devtools.reactors.%s' %
48 self.config['reactor'])
49 reactor = __import__(reactor_name, None, None, [''])
50 except ImportError:
51 raise TestError('The specified reactor is not supported.')
52 else:
53 try:
54 reactor.install(options=self.config)
55 except ImportError:
56 raise TestError(
57 'The Python package providing the requested reactor is '
58 'not installed. You can find it here: %s' %
59 reactor.REACTOR_URL)
60
61 mode = None
62 if self.config['debug']:
63 mode = TrialRunner.DEBUG
64 if self.config['dry-run']:
65 mode = TrialRunner.DRY_RUN
66
67 # Hook up to the parent test runner
68 super(TestRunner, self).__init__(
69 options=options,
70 reporterFactory=self.config['reporter'],
71 mode=mode,
72 profile=self.config['profile'],
73 logfile=self.config['logfile'],
74 tracebackFormat=self.config['tbformat'],
75 realTimeErrors=self.config['rterrors'],
76 uncleanWarnings=self.config['unclean-warnings'],
77 forceGarbageCollection=self.config['force-gc'])
78 # Named for trial compatibility.
79 # pylint: disable=C0103
80 self.workingDirectory = self.working_dir
81 # pylint: enable=C0103
82
83 def run_tests(self, suite):
84 """Run the twisted test suite."""
85 if self.config['until-failure']:
86 result = self.runUntilFailure(suite)
87 else:
88 result = self.run(suite)
89 return result.wasSuccessful()
90
91
92def _get_default_reactor():
93 """Return the platform-dependent default reactor to use."""
94 default_reactor = 'gi'
95 if sys.platform in ['darwin', 'win32']:
96 default_reactor = 'twisted'
97 return default_reactor
98
99
100class TestOptions(trial.Options, BaseTestOptions):
101 """Class for twisted options handling."""
102
103 optFlags = [["help-reactors", None],
104 ]
105
106 optParameters = [["reactor", "r", _get_default_reactor()],
107 ]
108
109 def __init__(self, *args, **kwargs):
110 super(TestOptions, self).__init__(*args, **kwargs)
111 self['rterrors'] = True
112
113 def opt_help_reactors(self):
114 """Help on available reactors for use with tests"""
115 synopsis = ('')
116 print(synopsis)
117 print('Need to get list of reactors and print them here.')
118 print()
119 sys.exit(0)
120
121 def opt_reactor(self, option):
122 """Which reactor to use (see --help-reactors for a list
123 of possibilities)
124 """
125 self['reactor'] = option
126 opt_r = opt_reactor
0127
=== modified file 'ubuntuone/devtools/testcases/dbus.py'
--- ubuntuone/devtools/testcases/dbus.py 2012-03-30 17:44:03 +0000
+++ ubuntuone/devtools/testcases/dbus.py 2012-08-02 15:49:22 +0000
@@ -97,7 +97,7 @@
97 bus_address = os.environ.get('DBUS_SESSION_BUS_ADDRESS', None)97 bus_address = os.environ.get('DBUS_SESSION_BUS_ADDRESS', None)
98 if os.path.dirname(unquote(bus_address.split(',')[0].split('=')[1])) \98 if os.path.dirname(unquote(bus_address.split(',')[0].split('=')[1])) \
99 != os.path.dirname(os.getcwd()):99 != os.path.dirname(os.getcwd()):
100 raise InvalidSessionBus('DBUS_SESSION_BUS_ADDRES is wrong.')100 raise InvalidSessionBus('DBUS_SESSION_BUS_ADDRESS is wrong.')
101101
102 # Set up the main loop and bus connection102 # Set up the main loop and bus connection
103 self.loop = DBusGMainLoop(set_as_default=True)103 self.loop = DBusGMainLoop(set_as_default=True)
104104
=== added file 'ubuntuone/devtools/utils.py'
--- ubuntuone/devtools/utils.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/utils.py 2012-08-02 15:49:22 +0000
@@ -0,0 +1,167 @@
1# Copyright 2009-2012 Canonical Ltd.
2#
3# This program is free software: you can redistribute it and/or modify it
4# under the terms of the GNU General Public License version 3, as published
5# by the Free Software Foundation.
6#
7# This program is distributed in the hope that it will be useful, but
8# WITHOUT ANY WARRANTY; without even the implied warranties of
9# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
10# PURPOSE. See the GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License along
13# with this program. If not, see <http://www.gnu.org/licenses/>.
14#
15# In addition, as a special exception, the copyright holders give
16# permission to link the code of portions of this program with the
17# OpenSSL library under certain conditions as described in each
18# individual source file, and distribute linked combinations
19# including the two.
20# You must obey the GNU General Public License in all respects
21# for all of the code used other than OpenSSL. If you modify
22# file(s) with this exception, you may extend this exception to your
23# version of the file(s), but you are not obligated to do so. If you
24# do not wish to do so, delete this exception statement from your
25# version. If you delete this exception statement from all source
26# files in the program, then also delete it here.
27"""Utilities for Ubuntu One developer tools."""
28
29import getopt
30import sys
31
32from ubuntuone.devtools.errors import UsageError
33__all__ = ['OptionParser']
34
35
36def accumulate_list_attr(class_obj, attr, list_obj, base_class=None):
37 """Get all of the list attributes of attr from the class hierarchy,
38 and return a list of the lists."""
39 for base in class_obj.__bases__:
40 accumulate_list_attr(base, attr, list_obj)
41 if base_class is None or base_class in class_obj.__bases__:
42 list_obj.extend(class_obj.__dict__.get(attr, []))
43
44
45def unpack_padded(length, sequence, default=None):
46 """Pads a sequence to length with value of default.
47
48 Returns a list containing the original and padded values.
49 """
50 newlist = [default] * length
51 newlist[:len(sequence)] = list(sequence)
52 return newlist
53
54
55class OptionParser(dict):
56 """Base options for our test runner."""
57
58 def __init__(self, *args, **kwargs):
59 super(OptionParser, self).__init__(*args, **kwargs)
60
61 # Store info about the options and defaults
62 self.long_opts = []
63 self.short_opts = ''
64 self.docs = {}
65 self.defaults = {}
66 self.dispatch = {}
67
68 # Get the options and defaults
69 for _get in [self._get_flags, self._get_params]:
70 # We don't use variable 'syns' here. It's just to pad the result.
71 # pylint: disable=W0612
72 (long_opts, short_opts, docs, defaults, syns, dispatch) = _get()
73 # pylint: enable=W0612
74 self.long_opts.extend(long_opts)
75 self.short_opts = self.short_opts + short_opts
76 self.docs.update(docs)
77 self.update(defaults)
78 self.defaults.update(defaults)
79 self.dispatch.update(dispatch)
80
81 # We use some camelcase names for trial compatibility here.
82 # pylint: disable=C0103
83 def parseOptions(self, options=None):
84 """Parse the options."""
85 if options is None:
86 options = sys.argv[1:]
87
88 try:
89 opts, args = getopt.getopt(options,
90 self.short_opts, self.long_opts)
91 except getopt.error as e:
92 raise UsageError(e)
93
94 for opt, arg in opts:
95 if opt[1] == '-':
96 opt = opt[2:]
97 else:
98 opt = opt[1:]
99
100 if (opt not in self.long_opts and
101 (len(opt) == 1 and opt not in self.short_opts)):
102 raise UsageError('No such options: "%s"' % opt)
103
104 self.dispatch[opt](arg)
105
106 try:
107 self.parseArgs(*args)
108 except TypeError:
109 raise UsageError('Wrong number of arguments.')
110
111 self.postOptions()
112
113 def postOptions(self):
114 """Called after options are parsed."""
115
116 def parseArgs(self, *args):
117 """Override to handle extra arguments specially."""
118 # pylint: enable=C0103
119
120 def _parse_arguments(self, has_default=False):
121 """Parse the arguments as either flags or parameters."""
122 long_opts, short_opts = [], ''
123 docs, defaults, dispatch = {}, {}, {}
124
125 _args = []
126 accumulate_list_attr(self.__class__, 'optFlags', _args)
127
128 for _arg in _args:
129 try:
130 if has_default:
131 l_opt, s_opt, default, doc = unpack_padded(4, _arg)
132 else:
133 default = False
134 l_opt, s_opt, doc = unpack_padded(3, _arg)
135 except ValueError:
136 raise ValueError('Failed to parse argument: %s' % _arg)
137 if not l_opt:
138 raise ValueError('An option must have a long name.')
139
140 opt_m_name = 'opt_' + l_opt.replace('-', '_')
141 opt_method = getattr(self, opt_m_name, None)
142 if opt_method is not None:
143 docs[l_opt] = getattr(opt_method, '__doc__', None)
144 dispatch[l_opt] = opt_method
145 if docs[l_opt] is None:
146 docs[l_opt] = doc
147 else:
148 docs[l_opt] = doc
149 dispatch[l_opt] = lambda arg: True
150
151 defaults[l_opt] = default
152 long_opts.append(l_opt)
153 if s_opt is not None:
154 short_opts = short_opts + s_opt
155 docs[s_opt] = docs[l_opt]
156 dispatch[s_opt] = dispatch[l_opt]
157 defaults[s_opt] = defaults[l_opt]
158
159 return long_opts, short_opts, docs, defaults, None, dispatch
160
161 def _get_flags(self):
162 """Get the flag options."""
163 return self._parse_arguments(has_default=False)
164
165 def _get_params(self):
166 """Get the parameters options."""
167 return self._parse_arguments(has_default=True)

Subscribers

People subscribed via source and target branches

to all changes: