Merge lp:~dobey/ubuntu/precise/ubuntuone-dev-tools/release-2-99-0 into lp:ubuntu/precise/ubuntuone-dev-tools

Proposed by dobey
Status: Merged
Merged at revision: 6
Proposed branch: lp:~dobey/ubuntu/precise/ubuntuone-dev-tools/release-2-99-0
Merge into: lp:ubuntu/precise/ubuntuone-dev-tools
Diff against target: 1560 lines (+869/-366)
17 files modified
PKG-INFO (+1/-1)
bin/u1lint (+10/-5)
bin/u1trial (+142/-86)
debian/changelog (+20/-0)
debian/control (+10/-9)
debian/pycompat (+0/-1)
debian/rules (+6/-11)
run-tests (+3/-2)
setup.py (+4/-2)
ubuntuone/devtools/reactors/glib.py (+1/-1)
ubuntuone/devtools/reactors/qt4.py (+9/-2)
ubuntuone/devtools/services/dbus.py (+1/-1)
ubuntuone/devtools/testcase.py (+11/-245)
ubuntuone/devtools/testcases/__init__.py (+166/-0)
ubuntuone/devtools/testcases/dbus.py (+118/-0)
ubuntuone/devtools/testing/__init__.py (+1/-0)
ubuntuone/devtools/testing/txcheck.py (+366/-0)
To merge this branch: bzr merge lp:~dobey/ubuntu/precise/ubuntuone-dev-tools/release-2-99-0
Reviewer Review Type Date Requested Status
Ken VanDine Approve
Ubuntu branches Pending
Review via email: mp+86756@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Daniel Holbach (dholbach) wrote :

Needs bug 907888 to be resolved first. changelog entry has wrong email address, but that not a big deal.

8. By dobey

Fix e-mail

9. By dobey

New upstream release.

10. By dobey

Fix dirspec dep to be 2.99.0 and not 3.1

Revision history for this message
Ken VanDine (ken-vandine) wrote :

Looks good

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'PKG-INFO'
--- PKG-INFO 2011-09-13 14:48:46 +0000
+++ PKG-INFO 2012-01-04 20:35:34 +0000
@@ -1,6 +1,6 @@
1Metadata-Version: 1.01Metadata-Version: 1.0
2Name: ubuntuone-dev-tools2Name: ubuntuone-dev-tools
3Version: 0.2.03Version: 2.99.1
4Summary: Ubuntu One development tools and utilities4Summary: Ubuntu One development tools and utilities
5Home-page: http://launchpad.net/ubuntuone-dev-tools5Home-page: http://launchpad.net/ubuntuone-dev-tools
6Author: UNKNOWN6Author: UNKNOWN
77
=== modified file 'bin/u1lint'
--- bin/u1lint 2011-09-13 14:48:46 +0000
+++ bin/u1lint 2012-01-04 20:35:34 +0000
@@ -4,7 +4,7 @@
4#4#
5# Author: Rodney Dawes <rodney.dawes@canonical.com>5# Author: Rodney Dawes <rodney.dawes@canonical.com>
6#6#
7# Copyright 2009-2010 Canonical Ltd.7# Copyright 2009-2011 Canonical Ltd.
8#8#
9# This program is free software: you can redistribute it and/or modify it9# This program is free software: you can redistribute it and/or modify it
10# under the terms of the GNU General Public License version 3, as published10# under the terms of the GNU General Public License version 3, as published
@@ -25,7 +25,7 @@
25import subprocess25import subprocess
26import sys26import sys
2727
28from xdg.BaseDirectory import xdg_data_dirs28from dirspec.basedir import xdg_data_dirs
2929
30SRCDIR = os.environ.get('SRCDIR', os.getcwd())30SRCDIR = os.environ.get('SRCDIR', os.getcwd())
3131
@@ -33,7 +33,7 @@
33class InvalidSetupException(Exception):33class InvalidSetupException(Exception):
34 """Raised when the env is not correctly setup."""34 """Raised when the env is not correctly setup."""
3535
36 36
37def find_python_installation_path():37def find_python_installation_path():
38 """Return the path where python was installed."""38 """Return the path where python was installed."""
39 assert(sys.platform == 'win32')39 assert(sys.platform == 'win32')
@@ -98,7 +98,8 @@
98 # the default is to assume that the script is executable and that it98 # the default is to assume that the script is executable and that it
99 # can be found in the path99 # can be found in the path
100 return [script, ]100 return [script, ]
101 101
102
102def find_pylintrc():103def find_pylintrc():
103 """Return the first pylintrc found."""104 """Return the first pylintrc found."""
104 # Use the pylintrc in the source tree if there is one105 # Use the pylintrc in the source tree if there is one
@@ -114,8 +115,10 @@
114 return full_name115 return full_name
115 return None116 return None
116117
118
117PYLINTRC = find_pylintrc()119PYLINTRC = find_pylintrc()
118120
121
119def _read_pylintrc_ignored():122def _read_pylintrc_ignored():
120 """Get the ignored files list from pylintrc"""123 """Get the ignored files list from pylintrc"""
121 try:124 try:
@@ -129,6 +132,7 @@
129 return []132 return []
130 # pylint: enable=E1103133 # pylint: enable=E1103
131134
135
132def _group_lines_by_file(data):136def _group_lines_by_file(data):
133 """Format file:line:message output as lines grouped by file."""137 """Format file:line:message output as lines grouped by file."""
134 did_fail = False138 did_fail = False
@@ -156,6 +160,7 @@
156160
157 return (did_fail, "\n".join(outputs))161 return (did_fail, "\n".join(outputs))
158162
163
159def _find_files():164def _find_files():
160 """Find all Python files under the current tree."""165 """Find all Python files under the current tree."""
161 pyfiles = []166 pyfiles = []
@@ -199,7 +204,7 @@
199 else:204 else:
200 pylint_args = get_subprocess_start_info('pylint')205 pylint_args = get_subprocess_start_info('pylint')
201 # append the extra args to the start info206 # append the extra args to the start info
202 pylint_args.extend(['--output-format=parseable', 207 pylint_args.extend(['--output-format=parseable',
203 '--include-ids=yes'])208 '--include-ids=yes'])
204 if PYLINTRC:209 if PYLINTRC:
205 pylint_args.append("--rcfile=" + PYLINTRC)210 pylint_args.append("--rcfile=" + PYLINTRC)
206211
=== modified file 'bin/u1trial'
--- bin/u1trial 2011-09-13 14:48:46 +0000
+++ bin/u1trial 2012-01-04 20:35:34 +0000
@@ -4,7 +4,7 @@
4#4#
5# Author: Rodney Dawes <rodney.dawes@canonical.com>5# Author: Rodney Dawes <rodney.dawes@canonical.com>
6#6#
7# Copyright 2009-2010 Canonical Ltd.7# Copyright 2009-2011 Canonical Ltd.
8#8#
9# This program is free software: you can redistribute it and/or modify it9# This program is free software: you can redistribute it and/or modify it
10# under the terms of the GNU General Public License version 3, as published10# under the terms of the GNU General Public License version 3, as published
@@ -28,11 +28,14 @@
28import sys28import sys
29import unittest29import unittest
3030
31from twisted.python.usage import UsageError
32from twisted.scripts import trial
31from twisted.trial.runner import TrialRunner33from twisted.trial.runner import TrialRunner
3234
33
34sys.path.insert(0, os.path.abspath("."))35sys.path.insert(0, os.path.abspath("."))
3536
37from ubuntuone.devtools.testing.txcheck import TXCheckSuite
38
3639
37def _is_in_ignored_path(testcase, paths):40def _is_in_ignored_path(testcase, paths):
38 """Return if the testcase is in one of the ignored paths."""41 """Return if the testcase is in one of the ignored paths."""
@@ -45,12 +48,10 @@
45class TestRunner(TrialRunner):48class TestRunner(TrialRunner):
46 """The test runner implementation."""49 """The test runner implementation."""
4750
48 def __init__(self, force_gc=False):51 def __init__(self, config=None):
49 from twisted.trial.reporter import TreeReporter
50
51 # set $HOME to the _trial_temp dir, to avoid breaking user files52 # set $HOME to the _trial_temp dir, to avoid breaking user files
52 trial_temp_dir = os.environ.get('TRIAL_TEMP_DIR', os.getcwd())53 trial_temp_dir = os.environ.get('TRIAL_TEMP_DIR', os.getcwd())
53 homedir = os.path.join(trial_temp_dir, '_trial_temp')54 homedir = os.path.join(trial_temp_dir, config['temp-directory'])
54 os.environ['HOME'] = homedir55 os.environ['HOME'] = homedir
5556
56 # setup $XDG_*_HOME variables and create the directories57 # setup $XDG_*_HOME variables and create the directories
@@ -71,12 +72,29 @@
71 # setup the ROOTDIR env var72 # setup the ROOTDIR env var
72 os.environ['ROOTDIR'] = os.getcwd()73 os.environ['ROOTDIR'] = os.getcwd()
7374
75 # Need an attribute for tempdir so we can use it later
74 self.tempdir = homedir76 self.tempdir = homedir
75 working_dir = os.path.join(self.tempdir, 'tmp')77 working_dir = os.path.join(self.tempdir, 'trial')
76 super(TestRunner, self).__init__(reporterFactory=TreeReporter,78
77 realTimeErrors=True,79 # Handle running trial in debug or dry-run mode
78 workingDirectory=working_dir,80 mode = None
79 forceGarbageCollection=force_gc)81 if config['debug']:
82 mode = TrialRunner.DEBUG
83 if config['dry-run']:
84 mode = TrialRunner.DRY_RUN
85
86 # Hook up to the parent test runner
87 super(TestRunner, self).__init__(
88 reporterFactory=config['reporter'],
89 mode=mode,
90 profile=config['profile'],
91 logfile=config['logfile'],
92 tracebackFormat=config['tbformat'],
93 realTimeErrors=config['rterrors'],
94 uncleanWarnings=config['unclean-warnings'],
95 workingDirectory=working_dir,
96 forceGarbageCollection=config['force-gc'])
97
80 self.required_services = []98 self.required_services = []
81 self.source_files = []99 self.source_files = []
82100
@@ -112,23 +130,11 @@
112 def _collect_tests(self, path, test_pattern, ignored_modules,130 def _collect_tests(self, path, test_pattern, ignored_modules,
113 ignored_paths):131 ignored_paths):
114 """Return the set of unittests."""132 """Return the set of unittests."""
115 suite = unittest.TestSuite()133 suite = TXCheckSuite()
116 if test_pattern:134 if test_pattern:
117 pattern = re.compile('.*%s.*' % test_pattern)135 pattern = re.compile('.*%s.*' % test_pattern)
118 else:136 else:
119 pattern = None137 pattern = None
120
121 # get the ignored modules/tests
122 if ignored_modules:
123 ignored_modules = map(str.strip, ignored_modules.split(','))
124 else:
125 ignored_modules = []
126
127 # get the ignored paths
128 if ignored_paths:
129 ignored_paths = map(str.strip, ignored_paths.split(','))
130 else:
131 ignored_paths = []
132138
133 # Disable this lint warning as we need to access _tests in the139 # Disable this lint warning as we need to access _tests in the
134 # test suites, to collect the tests140 # test suites, to collect the tests
@@ -169,100 +175,150 @@
169 suite.addTests(module_suite)175 suite.addTests(module_suite)
170 return suite176 return suite
171177
172 # pylint: disable=E0202178 def get_suite(self, config):
173 def run(self, args, options=None):179 """Get the test suite to use."""
180 suite = unittest.TestSuite()
181 for path in config['tests']:
182 suite.addTest(self._collect_tests(path, config['test'],
183 config['ignore-modules'],
184 config['ignore-paths']))
185 if config['loop']:
186 old_suite = suite
187 suite = unittest.TestSuite()
188 for _ in xrange(config['loop']):
189 suite.addTest(old_suite)
190
191 return suite
192
193 # pylint: disable=C0103
194 def _runWithoutDecoration(self, test):
174 """run the tests."""195 """run the tests."""
175 success = 0196 result = None
176 running_services = []197 running_services = []
177 if options.coverage:
178 coverage.erase()
179 coverage.start()
180198
181 try:199 try:
182 suite = unittest.TestSuite()
183 for path in args:
184 print "Adding path"
185 suite.addTest(self._collect_tests(path, options.test,
186 options.ignored_modules,
187 options.ignored_paths))
188 if options.loops:
189 old_suite = suite
190 suite = unittest.TestSuite()
191 for _ in xrange(options.loops):
192 suite.addTest(old_suite)
193
194 # Start any required services200 # Start any required services
195 for service in self.required_services:201 for service in self.required_services:
196 runner = service()202 runner = service()
197 runner.start_service(tempdir=self.tempdir)203 runner.start_service(tempdir=self.tempdir)
198 running_services.append(runner)204 running_services.append(runner)
199205
200 result = super(TestRunner, self).run(suite)206 result = super(TestRunner, self)._runWithoutDecoration(test)
201 success = result.wasSuccessful()
202 finally:207 finally:
203 # Stop all the running services208 # Stop all the running services
204 for runner in running_services:209 for runner in running_services:
205 runner.stop_service()210 runner.stop_service()
206211
207 if options.coverage:212 return result
208 coverage.stop()213
209 coverage.report(self.source_files, ignore_errors=True,214
210 show_missing=False)215class Options(trial.Options):
211216 """Class for options handling."""
212 if not success:217
213 sys.exit(1)218 optFlags = [["coverage", "c"],
214 else:219 ["gui", None],
215 sys.exit(0)220 ["help-reactors", None],
221 ]
222
223 optParameters = [["test", "t", None],
224 ["loop", None, 1],
225 ["ignore-modules", "i", ""],
226 ["ignore-paths", "p", ""],
227 ["reactor", "r", "glib"],
228 ]
229
230 def __init__(self):
231 self['tests'] = set()
232 super(Options, self).__init__()
233 self['rterrors'] = True
234
235 def opt_coverage(self):
236 """Generate a coverage report for the run tests"""
237 self['coverage'] = True
238
239 def opt_gui(self):
240 """Use the GUI mode of some reactors"""
241 self['gui'] = True
242
243 def opt_help_reactors(self):
244 """Help on available reactors for use with tests"""
245 synopsis = ('')
246 print synopsis
247 print 'Need to get list of reactors and print them here.'
248 print
249 sys.exit(0)
250
251 def opt_test(self, option):
252 """Run specific tests, e.g: className.methodName"""
253 self['test'] = option
254
255 def opt_loop(self, option):
256 """Loop tests the specified number of times."""
257 try:
258 self['loop'] = long(option)
259 except ValueError:
260 raise UsageError('A positive integer value must be specified.')
261
262 def opt_ignore_modules(self, option):
263 """Comma-separate list of test modules to ignore,
264 e.g: test_gtk.py, test_account.py
265 """
266 self['ignore-modules'] = map(str.strip, option.split(','))
267
268 def opt_ignore_paths(self, option):
269 """Comma-separated list of relative paths to ignore,
270 e.g: tests/platform/windows, tests/platform/macosx
271 """
272 self['ignore-paths'] = map(str.strip, option.split(','))
273
274 def opt_reactor(self, option):
275 """Which reactor to use (see --help-reactors for a list
276 of possibilities)
277 """
278 self['reactor'] = option
279 opt_r = opt_reactor
216280
217281
218def main():282def main():
219 """Do the deed."""283 """Do the deed."""
220 from optparse import OptionParser284 if len(sys.argv) == 1:
221 usage = '%prog [options] path'285 sys.argv.append('--help')
222 parser = OptionParser(usage=usage)286
223 parser.add_option("-t", "--test", dest="test",287 config = Options()
224 help = "run specific tests, e.g: className.methodName")288 config.parseOptions()
225 parser.add_option("-l", "--loop", dest="loops", type="int", default=1,
226 help = "loop selected tests LOOPS number of times",
227 metavar="LOOPS")
228 parser.add_option("-c", "--coverage", action="store_true", dest="coverage",
229 help="print a coverage report when finished")
230 parser.add_option("-i", "--ignored-modules", dest="ignored_modules",
231 default=None, help="comma-separated test moodules "
232 + "to ignore, e.g: test_gtk.py, test_account.py")
233 parser.add_option("-p", "--ignore-paths", dest="ignored_paths",
234 default=None, help="comma-separated relative "
235 + "paths to ignore. "
236 + "e.g: tests/platform/windows, tests/platform/macosx")
237 parser.add_option("--force-gc", action="store_true", dest="force_gc",
238 default=False, help="Run gc.collect() before and after "
239 "each test case.")
240 parser.add_option("--reactor", type="string", dest="reactor",
241 default='glib',
242 help="Run the tests using the specified reactor.",
243 metavar="REACTOR")
244 parser.add_option("--gui", action="store_true", dest="use_gui",
245 help="Use the GUI mode of some reactors.")
246 (options, args) = parser.parse_args()
247 if not args:
248 parser.print_help()
249 sys.exit(2)
250289
251 try:290 try:
252 reactor_name = 'ubuntuone.devtools.reactors.%s' % options.reactor291 reactor_name = 'ubuntuone.devtools.reactors.%s' % config['reactor']
253 reactor = __import__(reactor_name, None, None, [''])292 reactor = __import__(reactor_name, None, None, [''])
254 except ImportError:293 except ImportError:
255 print 'The specified reactor is not supported.'294 print 'The specified reactor is not supported.'
256 sys.exit(1)295 sys.exit(1)
257 else:296 else:
258 try:297 try:
259 reactor.install(options=options)298 reactor.install(options=config)
260 except ImportError:299 except ImportError:
261 print('The Python package providing the requested reactor is not '300 print('The Python package providing the requested reactor is not '
262 'installed. You can find it here: %s' % reactor.REACTOR_URL)301 'installed. You can find it here: %s' % reactor.REACTOR_URL)
263 raise302 raise
264303
265 TestRunner(force_gc=options.force_gc).run(args, options)304 trial_runner = TestRunner(config=config)
305 suite = trial_runner.get_suite(config)
306
307 if config['coverage']:
308 coverage.erase()
309 coverage.start()
310
311 if config['until-failure']:
312 result = trial_runner.runUntilFailure(suite)
313 else:
314 result = trial_runner.run(suite)
315
316 if config['coverage']:
317 coverage.stop()
318 coverage.report(trial_runner.source_files, ignore_errors=True,
319 show_missing=False)
320
321 sys.exit(not result.wasSuccessful())
266322
267323
268if __name__ == '__main__':324if __name__ == '__main__':
269325
=== modified file 'debian/changelog'
--- debian/changelog 2011-09-15 20:52:05 +0000
+++ debian/changelog 2012-01-04 20:35:34 +0000
@@ -1,3 +1,23 @@
1ubuntuone-dev-tools (2.99.1-0ubuntu1) precise; urgency=low
2
3 * New upstream release.
4
5 -- Rodney Dawes <rodney.dawes@ubuntu.com> Wed, 04 Jan 2012 13:12:03 -0500
6
7ubuntuone-dev-tools (2.99.0-0ubuntu1) precise; urgency=low
8
9 * New upstream release.
10 - Depends on dirspec now (LP: #888619)
11 - Pass options through to trial (LP: #890786)
12 * debian/control:
13 - Move python-gobject to Suggests
14 - Suggest python-qt4reactor
15 * debian/rules:
16 - Convert to pure dh
17 - Run unit tests during build
18
19 -- Rodney Dawes <rodney.dawes@ubuntu.com> Thu, 22 Dec 2011 16:33:22 -0500
20
1ubuntuone-dev-tools (0.2.0-0ubuntu1) oneiric; urgency=low21ubuntuone-dev-tools (0.2.0-0ubuntu1) oneiric; urgency=low
222
3 * New upstream release.23 * New upstream release.
424
=== modified file 'debian/control'
--- debian/control 2011-09-15 20:52:05 +0000
+++ debian/control 2012-01-04 20:35:34 +0000
@@ -3,27 +3,29 @@
3XSBC-Original-Maintainer: Rodney Dawes <rodney.dawes@ubuntu.com>3XSBC-Original-Maintainer: Rodney Dawes <rodney.dawes@ubuntu.com>
4Section: python4Section: python
5Priority: optional5Priority: optional
6Standards-Version: 3.9.16Standards-Version: 3.9.2
7X-Python-Version: >= 2.6
7Build-Depends-Indep:8Build-Depends-Indep:
8 dbus,9 dbus,
9 pep8,10 pep8,
10 pylint (>= 0.21.0),11 pylint (>= 0.21.0),
11 python-coverage,12 python-coverage,
12 python-dbus,13 python-dbus,
14 python-dirspec (>= 2.99.0),
13 python-gobject,15 python-gobject,
14 python-qt4,16 python-qt4,
15 python-setuptools,17 python-setuptools,
16 python-support,18 python-twisted-core
17 python-twisted-core,19Build-Depends: debhelper (>= 7.0.50),
18 python-xdg20 python-all (>= 2.6.6-3)
19Build-Depends: cdbs (>= 0.4.43), debhelper (>= 7.0.17), python-all
20Homepage: http://launchpad.net/ubuntuone-dev-tools21Homepage: http://launchpad.net/ubuntuone-dev-tools
2122
22Package: python-ubuntuone-devtools23Package: python-ubuntuone-devtools
23Architecture: all24Architecture: all
24Depends: ${python:Depends}, ${misc:Depends},25Depends: ${python:Depends}, ${misc:Depends},
25 dbus,26 dbus,
26 python-dbus27 python-dbus,
28 python-dirspec (>= 2.99.0)
27Description: Ubuntu One development tools - Python modules29Description: Ubuntu One development tools - Python modules
28 Ubuntu One development tools provides scripts, test cases, and other30 Ubuntu One development tools provides scripts, test cases, and other
29 utilities for developing Python projects which need more integration31 utilities for developing Python projects which need more integration
@@ -35,10 +37,9 @@
35Depends: ${python:Depends}, ${misc:Depends}, python,37Depends: ${python:Depends}, ${misc:Depends}, python,
36 pylint (>= 0.21.0) | pyflakes,38 pylint (>= 0.21.0) | pyflakes,
37 python-coverage,39 python-coverage,
38 python-gobject,
39 python-twisted-core,40 python-twisted-core,
40 python-ubuntuone-devtools (= ${binary:Version}),41 python-ubuntuone-devtools (= ${binary:Version})
41 python-xdg42Suggests: python-gobject, python-qt4reactor
42Description: Ubuntu One development tools43Description: Ubuntu One development tools
43 Ubuntu One development tools provides scripts, test cases, and other44 Ubuntu One development tools provides scripts, test cases, and other
44 utilities for developing Python projects which need more integration45 utilities for developing Python projects which need more integration
4546
=== removed file 'debian/pycompat'
--- debian/pycompat 2010-08-02 13:45:54 +0000
+++ debian/pycompat 1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
12
20
=== modified file 'debian/rules'
--- debian/rules 2010-11-30 18:59:50 +0000
+++ debian/rules 2012-01-04 20:35:34 +0000
@@ -1,13 +1,8 @@
1#!/usr/bin/make -f1#!/usr/bin/make -f
22
3include /usr/share/cdbs/1/rules/debhelper.mk3%:
4DEB_PYTHON_SYSTEM=pysupport4 dh --with python2 $@
5include /usr/share/cdbs/1/class/python-distutils.mk5
66override_dh_auto_test:
7common-build-arch common-build-indep:: debian/stamp-check7 ./run-tests
8debian/stamp-check:8
9 cd $(DEB_SRCDIR) && ./run-tests
10 touch $@
11
12makefile-clean::
13 rm -f debian/stamp-check
149
=== modified file 'run-tests'
--- run-tests 2011-09-13 14:48:46 +0000
+++ run-tests 2012-01-04 20:35:34 +0000
@@ -1,7 +1,7 @@
1#!/bin/bash1#!/bin/bash
2# Author: Natalia Bidart <natalia.bidart@canonical.com>2# Author: Natalia Bidart <natalia.bidart@canonical.com>
3#3#
4# Copyright 2010 Canonical Ltd.4# Copyright 2010-2011 Canonical Ltd.
5#5#
6# This program is free software: you can redistribute it and/or modify it6# This program is free software: you can redistribute it and/or modify it
7# under the terms of the GNU General Public License version 3, as published7# under the terms of the GNU General Public License version 3, as published
@@ -18,7 +18,8 @@
1818
19bin/u1trial -c ubuntuone19bin/u1trial -c ubuntuone
20bin/u1trial --reactor=twisted ubuntuone20bin/u1trial --reactor=twisted ubuntuone
21echo "Running style checks..."
21bin/u1lint22bin/u1lint
22pep8 --repeat .23pep8 --repeat . bin/*
23rm -rf _trial_temp24rm -rf _trial_temp
24rm -rf .coverage25rm -rf .coverage
2526
=== modified file 'setup.py'
--- setup.py 2011-09-13 14:48:46 +0000
+++ setup.py 2012-01-04 20:35:34 +0000
@@ -21,7 +21,7 @@
21from distutils.core import setup, Command21from distutils.core import setup, Command
2222
23PACKAGE = 'ubuntuone-dev-tools'23PACKAGE = 'ubuntuone-dev-tools'
24VERSION = '0.2.0'24VERSION = '2.99.1'
2525
26U1LINT = 'bin/u1lint'26U1LINT = 'bin/u1lint'
2727
@@ -51,7 +51,9 @@
51 packages=['ubuntuone',51 packages=['ubuntuone',
52 'ubuntuone.devtools',52 'ubuntuone.devtools',
53 'ubuntuone.devtools.reactors',53 'ubuntuone.devtools.reactors',
54 'ubuntuone.devtools.services'],54 'ubuntuone.devtools.services',
55 'ubuntuone.devtools.testing',
56 'ubuntuone.devtools.testcases'],
55 extra_path='ubuntuone-dev-tools',57 extra_path='ubuntuone-dev-tools',
56 scripts=['bin/u1lint',58 scripts=['bin/u1lint',
57 'bin/u1trial',59 'bin/u1trial',
5860
=== modified file 'ubuntuone/devtools/reactors/glib.py'
--- ubuntuone/devtools/reactors/glib.py 2011-09-13 14:48:46 +0000
+++ ubuntuone/devtools/reactors/glib.py 2012-01-04 20:35:34 +0000
@@ -17,7 +17,7 @@
17def install(options=None):17def install(options=None):
18 """Install the reactor and parse any options we might need."""18 """Install the reactor and parse any options we might need."""
19 reactor_name = None19 reactor_name = None
20 if options is not None and options.use_gui:20 if options is not None and options['gui']:
21 reactor_name = 'twisted.internet.gtk2reactor'21 reactor_name = 'twisted.internet.gtk2reactor'
22 else:22 else:
23 reactor_name = 'twisted.internet.glib2reactor'23 reactor_name = 'twisted.internet.glib2reactor'
2424
=== modified file 'ubuntuone/devtools/reactors/qt4.py'
--- ubuntuone/devtools/reactors/qt4.py 2011-09-13 14:48:46 +0000
+++ ubuntuone/devtools/reactors/qt4.py 2012-01-04 20:35:34 +0000
@@ -22,12 +22,19 @@
2222
23def install(options=None):23def install(options=None):
24 """Install the reactor and parse any options we might need."""24 """Install the reactor and parse any options we might need."""
25 if options is not None and options.use_gui:25 if options is not None and options['gui']:
26 from PyQt4.QtGui import QApplication26 from PyQt4.QtGui import QApplication
27 # We must assign this to a variable, or we will get crashes in Qt27 # We must assign this to a variable, or we will get crashes in Qt
28 # pylint: disable=W061228 # pylint: disable=W0612
29 app = QApplication(sys.argv)29 app = QApplication(sys.argv)
30 # pylint: enable=W061230 # pylint: enable=W0612
3131
32 qt4reactor = __import__('qtreactor.qt4reactor', None, None, [''])32 try:
33 qt4reactor = __import__('qt4reactor', None, None, [''])
34 except ImportError:
35 # Leave this import in place for a couple of weeks
36 # until all the devs are using the packaged qt4reactor
37 # (nessita, 11-11-11)
38 qt4reactor = __import__('qtreactor.qt4reactor', None, None, [''])
39
33 qt4reactor.install()40 qt4reactor.install()
3441
=== modified file 'ubuntuone/devtools/services/dbus.py'
--- ubuntuone/devtools/services/dbus.py 2011-09-13 14:48:46 +0000
+++ ubuntuone/devtools/services/dbus.py 2012-01-04 20:35:34 +0000
@@ -20,9 +20,9 @@
20import signal20import signal
21import subprocess21import subprocess
2222
23from dirspec.basedir import load_data_paths
23from distutils.spawn import find_executable24from distutils.spawn import find_executable
24from urllib import quote25from urllib import quote
25from xdg.BaseDirectory import load_data_paths
2626
2727
28class DBusLaunchError(Exception):28class DBusLaunchError(Exception):
2929
=== modified file 'ubuntuone/devtools/testcase.py'
--- ubuntuone/devtools/testcase.py 2011-09-13 14:48:46 +0000
+++ ubuntuone/devtools/testcase.py 2012-01-04 20:35:34 +0000
@@ -1,8 +1,6 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
2
3# Author: Guillermo Gonzalez <guillermo.gonzalez@canonical.com>
4#2#
5# Copyright 2009-2010 Canonical Ltd.3# Copyright 2011 Canonical Ltd.
6#4#
7# This program is free software: you can redistribute it and/or modify it5# This program is free software: you can redistribute it and/or modify it
8# under the terms of the GNU General Public License version 3, as published6# under the terms of the GNU General Public License version 3, as published
@@ -15,245 +13,13 @@
15#13#
16# You should have received a copy of the GNU General Public License along14# You should have received a copy of the GNU General Public License along
17# with this program. If not, see <http://www.gnu.org/licenses/>.15# with this program. If not, see <http://www.gnu.org/licenses/>.
1816"""Maintain old API."""
19"""Base tests cases and test utilities."""17
2018import warnings
21from __future__ import with_statement19warnings.warn(
2220 'Deprecated import path; use ubuntuone.devtools.testcases'
23import contextlib21 'instead.', DeprecationWarning, stacklevel=2)
24import os22
25import shutil23# pylint: disable=W0401,W0614
26import sys24from ubuntuone.devtools.testcases import *
2725from ubuntuone.devtools.testcases.dbus import *
28from functools import wraps
29
30from twisted.internet import defer
31from twisted.trial.unittest import TestCase, SkipTest
32
33# DBusRunner for DBusTestCase using tests
34from ubuntuone.devtools.services.dbus import DBusRunner
35
36
37# pylint: disable=F0401,C0103
38try:
39 import dbus
40except ImportError:
41 dbus = None
42
43try:
44 import dbus.service as service
45except ImportError:
46 service = None
47
48try:
49 from dbus.mainloop.glib import DBusGMainLoop
50except ImportError:
51 DBusGMainLoop = None
52
53
54# pylint: enable=F0401,C0103
55@contextlib.contextmanager
56def environ(env_var, new_value):
57 """context manager to replace/add an environ value"""
58 old_value = os.environ.get(env_var, None)
59 os.environ[env_var] = new_value
60 yield
61 if old_value is None:
62 os.environ.pop(env_var)
63 else:
64 os.environ[env_var] = old_value
65
66
67def _id(obj):
68 """Return the obj calling the funct."""
69 return obj
70
71
72# pylint: disable=C0103
73def skipTest(reason):
74 """Unconditionally skip a test."""
75
76 def decorator(test_item):
77 """Decorate the test so that it is skipped."""
78 if not (isinstance(test_item, type) and\
79 issubclass(test_item, TestCase)):
80
81 @wraps(test_item)
82 def skip_wrapper(*args, **kwargs):
83 """Skip a test method raising an exception."""
84 raise SkipTest(reason)
85 test_item = skip_wrapper
86
87 # tell twisted.trial.unittest to skip the test, pylint will complain
88 # since it thinks we are redefining a name out of the scope
89 # pylint: disable=W0621,W0612
90 test_item.skip = reason
91 # pylint: enable=W0621,W0612
92 # because the item was skipped, we will make sure that no
93 # services are started for it
94 if hasattr(test_item, "required_services"):
95 # pylint: disable=W0612
96 test_item.required_services = lambda *args, **kwargs: []
97 # pylint: enable=W0612
98 return test_item
99 return decorator
100
101
102def skipIf(condition, reason):
103 """Skip a test if the condition is true."""
104 if condition:
105 return skipTest(reason)
106 return _id
107
108
109def skipIfOS(current_os, reason):
110 """Skip test for a particular os or lists of them."""
111 if os:
112 if sys.platform in current_os or sys.platform == current_os:
113 return skipTest(reason)
114 return _id
115 return _id
116
117
118def skipIfNotOS(current_os, reason):
119 """Skip test we are not in a particular os."""
120 if os:
121 if sys.platform not in current_os or\
122 sys.platform != current_os:
123 return skipTest(reason)
124 return _id
125 return _id
126
127
128# pylint: enable=C0103
129
130
131class InvalidSessionBus(Exception):
132 """Error when we are connected to the wrong session bus in tests."""
133
134
135class FakeDBusInterface(object):
136 """A fake DBusInterface..."""
137
138 def shutdown(self, with_restart=False):
139 """...that only knows how to go away"""
140
141
142class BaseTestCase(TestCase):
143 """Base TestCase with helper methods to handle temp dir.
144
145 This class provides:
146 mktemp(name): helper to create temporary dirs
147 rmtree(path): support read-only shares
148 makedirs(path): support read-only shares
149
150 """
151
152 def required_services(self):
153 """Return the list of required services for DBusTestCase."""
154 return []
155
156 def mktemp(self, name='temp'):
157 """Customized mktemp that accepts an optional name argument."""
158 tempdir = os.path.join(self.tmpdir, name)
159 if os.path.exists(tempdir):
160 self.rmtree(tempdir)
161 self.makedirs(tempdir)
162 return tempdir
163
164 @property
165 def tmpdir(self):
166 """Default tmpdir: module/class/test_method."""
167 # check if we already generated the root path
168 root_dir = getattr(self, '__root', None)
169 if root_dir:
170 return root_dir
171 max_filename = 32 # some platforms limit lengths of filenames
172 base = os.path.join(self.__class__.__module__[:max_filename],
173 self.__class__.__name__[:max_filename],
174 self._testMethodName[:max_filename])
175 # use _trial_temp dir, it should be os.gwtcwd()
176 # define the root temp dir of the testcase, pylint: disable=W0201
177 self.__root = os.path.join(os.getcwd(), base)
178 return self.__root
179
180 def rmtree(self, path):
181 """Custom rmtree that handle ro parent(s) and childs."""
182 if not os.path.exists(path):
183 return
184 # change perms to rw, so we can delete the temp dir
185 if path != getattr(self, '__root', None):
186 os.chmod(os.path.dirname(path), 0755)
187 if not os.access(path, os.W_OK):
188 os.chmod(path, 0755)
189 # pylint: disable=W0612
190 for dirpath, dirs, files in os.walk(path):
191 for dirname in dirs:
192 if not os.access(os.path.join(dirpath, dirname), os.W_OK):
193 os.chmod(os.path.join(dirpath, dirname), 0777)
194 shutil.rmtree(path)
195
196 def makedirs(self, path):
197 """Custom makedirs that handle ro parent."""
198 parent = os.path.dirname(path)
199 if os.path.exists(parent):
200 os.chmod(parent, 0755)
201 os.makedirs(path)
202
203
204@skipIf(dbus is None or service is None or DBusGMainLoop is None,
205 "The test requires dbus.")
206class DBusTestCase(BaseTestCase):
207 """Test the DBus event handling."""
208
209 def required_services(self):
210 """Return the list of required services for DBusTestCase."""
211 services = super(DBusTestCase, self).required_services()
212 services.extend([DBusRunner])
213 return services
214
215 @defer.inlineCallbacks
216 def setUp(self):
217 """Setup the infrastructure fo the test (dbus service)."""
218 # Class 'BaseTestCase' has no 'setUp' member
219 # pylint: disable=E1101
220 # dbus modules will be imported by the decorator
221 # pylint: disable=E0602
222 yield super(DBusTestCase, self).setUp()
223
224 # We need to ensure DBUS_SESSION_BUS_ADDRESS is private here
225 from urllib import unquote
226 bus_address = os.environ.get('DBUS_SESSION_BUS_ADDRESS', None)
227 if os.path.dirname(unquote(bus_address.split(',')[0].split('=')[1])) \
228 != os.path.dirname(os.getcwd()):
229 raise InvalidSessionBus('DBUS_SESSION_BUS_ADDRES is wrong.')
230
231 # Set up the main loop and bus connection
232 self.loop = DBusGMainLoop(set_as_default=True)
233 self.bus = dbus.bus.BusConnection(address_or_type=bus_address,
234 mainloop=self.loop)
235
236 # Monkeypatch the dbus.SessionBus/SystemBus methods, to ensure we
237 # always point at our own private bus instance.
238 self.patch(dbus, 'SessionBus', lambda: self.bus)
239 self.patch(dbus, 'SystemBus', lambda: self.bus)
240
241 # Check that we are on the correct bus for real
242# Disable this for now, because our tests are extremely broken :(
243# bus_names = self.bus.list_names()
244# if len(bus_names) > 2:
245# raise InvalidSessionBus('Too many bus connections: %s (%r)' %
246# (len(bus_names), bus_names))
247
248 # monkeypatch busName.__del__ to avoid errors on gc
249 # we take care of releasing the name in shutdown
250 service.BusName.__del__ = lambda _: None
251 yield self.bus.set_exit_on_disconnect(False)
252 self.signal_receivers = set()
253
254 @defer.inlineCallbacks
255 def tearDown(self):
256 """Cleanup the test."""
257 yield self.bus.flush()
258 yield self.bus.close()
259 yield super(DBusTestCase, self).tearDown()
26026
=== added directory 'ubuntuone/devtools/testcases'
=== added file 'ubuntuone/devtools/testcases/__init__.py'
--- ubuntuone/devtools/testcases/__init__.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/testcases/__init__.py 2012-01-04 20:35:34 +0000
@@ -0,0 +1,166 @@
1# -*- coding: utf-8 -*-
2#
3# Copyright 2009-2011 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranties of
11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12# PURPOSE. See the GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program. If not, see <http://www.gnu.org/licenses/>.
16
17"""Base tests cases and test utilities."""
18
19from __future__ import with_statement
20
21import contextlib
22import os
23import shutil
24import sys
25
26from functools import wraps
27
28from twisted.trial.unittest import TestCase, SkipTest
29
30
31@contextlib.contextmanager
32def environ(env_var, new_value):
33 """context manager to replace/add an environ value"""
34 old_value = os.environ.get(env_var, None)
35 os.environ[env_var] = new_value
36 yield
37 if old_value is None:
38 os.environ.pop(env_var)
39 else:
40 os.environ[env_var] = old_value
41
42
43def _id(obj):
44 """Return the obj calling the funct."""
45 return obj
46
47
48# pylint: disable=C0103
49def skipTest(reason):
50 """Unconditionally skip a test."""
51
52 def decorator(test_item):
53 """Decorate the test so that it is skipped."""
54 if not (isinstance(test_item, type) and\
55 issubclass(test_item, TestCase)):
56
57 @wraps(test_item)
58 def skip_wrapper(*args, **kwargs):
59 """Skip a test method raising an exception."""
60 raise SkipTest(reason)
61 test_item = skip_wrapper
62
63 # tell twisted.trial.unittest to skip the test, pylint will complain
64 # since it thinks we are redefining a name out of the scope
65 # pylint: disable=W0621,W0612
66 test_item.skip = reason
67 # pylint: enable=W0621,W0612
68 # because the item was skipped, we will make sure that no
69 # services are started for it
70 if hasattr(test_item, "required_services"):
71 # pylint: disable=W0612
72 test_item.required_services = lambda *args, **kwargs: []
73 # pylint: enable=W0612
74 return test_item
75 return decorator
76
77
78def skipIf(condition, reason):
79 """Skip a test if the condition is true."""
80 if condition:
81 return skipTest(reason)
82 return _id
83
84
85def skipIfOS(current_os, reason):
86 """Skip test for a particular os or lists of them."""
87 if os:
88 if sys.platform in current_os or sys.platform == current_os:
89 return skipTest(reason)
90 return _id
91 return _id
92
93
94def skipIfNotOS(current_os, reason):
95 """Skip test we are not in a particular os."""
96 if os:
97 if sys.platform not in current_os or\
98 sys.platform != current_os:
99 return skipTest(reason)
100 return _id
101 return _id
102
103
104# pylint: enable=C0103
105
106
107class BaseTestCase(TestCase):
108 """Base TestCase with helper methods to handle temp dir.
109
110 This class provides:
111 mktemp(name): helper to create temporary dirs
112 rmtree(path): support read-only shares
113 makedirs(path): support read-only shares
114
115 """
116
117 def required_services(self):
118 """Return the list of required services for DBusTestCase."""
119 return []
120
121 def mktemp(self, name='temp'):
122 """Customized mktemp that accepts an optional name argument."""
123 tempdir = os.path.join(self.tmpdir, name)
124 if os.path.exists(tempdir):
125 self.rmtree(tempdir)
126 self.makedirs(tempdir)
127 return tempdir
128
129 @property
130 def tmpdir(self):
131 """Default tmpdir: module/class/test_method."""
132 # check if we already generated the root path
133 root_dir = getattr(self, '__root', None)
134 if root_dir:
135 return root_dir
136 max_filename = 32 # some platforms limit lengths of filenames
137 base = os.path.join(self.__class__.__module__[:max_filename],
138 self.__class__.__name__[:max_filename],
139 self._testMethodName[:max_filename])
140 # use _trial_temp dir, it should be os.gwtcwd()
141 # define the root temp dir of the testcase, pylint: disable=W0201
142 self.__root = os.path.join(os.getcwd(), base)
143 return self.__root
144
145 def rmtree(self, path):
146 """Custom rmtree that handle ro parent(s) and childs."""
147 if not os.path.exists(path):
148 return
149 # change perms to rw, so we can delete the temp dir
150 if path != getattr(self, '__root', None):
151 os.chmod(os.path.dirname(path), 0755)
152 if not os.access(path, os.W_OK):
153 os.chmod(path, 0755)
154 # pylint: disable=W0612
155 for dirpath, dirs, files in os.walk(path):
156 for dirname in dirs:
157 if not os.access(os.path.join(dirpath, dirname), os.W_OK):
158 os.chmod(os.path.join(dirpath, dirname), 0777)
159 shutil.rmtree(path)
160
161 def makedirs(self, path):
162 """Custom makedirs that handle ro parent."""
163 parent = os.path.dirname(path)
164 if os.path.exists(parent):
165 os.chmod(parent, 0755)
166 os.makedirs(path)
0167
=== added file 'ubuntuone/devtools/testcases/dbus.py'
--- ubuntuone/devtools/testcases/dbus.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/testcases/dbus.py 2012-01-04 20:35:34 +0000
@@ -0,0 +1,118 @@
1# -*- coding: utf-8 -*-
2#
3# Copyright 2009-2011 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranties of
11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12# PURPOSE. See the GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program. If not, see <http://www.gnu.org/licenses/>.
16
17"""Base tests cases and test utilities."""
18
19from __future__ import absolute_import, with_statement
20
21import os
22
23from twisted.internet import defer
24
25# lint seems not tow work well when we use decorators
26# pylint:disable=W0611
27from ubuntuone.devtools.testcase import BaseTestCase, skipIf
28# pylint:enable=W0611
29# DBusRunner for DBusTestCase using tests
30from ubuntuone.devtools.services.dbus import DBusRunner
31
32
33# pylint: disable=F0401,C0103,W0406,E0611
34try:
35 import dbus
36except ImportError, e:
37 dbus = None
38
39try:
40 import dbus.service as service
41except ImportError:
42 service = None
43
44try:
45 from dbus.mainloop.glib import DBusGMainLoop
46except ImportError:
47 DBusGMainLoop = None
48
49# pylint: enable=F0401,C0103,W0406,E0611
50
51
52class InvalidSessionBus(Exception):
53 """Error when we are connected to the wrong session bus in tests."""
54
55
56class FakeDBusInterface(object):
57 """A fake DBusInterface..."""
58
59 def shutdown(self, with_restart=False):
60 """...that only knows how to go away"""
61
62
63@skipIf(dbus is None or service is None or DBusGMainLoop is None,
64 "The test requires dbus.")
65class DBusTestCase(BaseTestCase):
66 """Test the DBus event handling."""
67
68 def required_services(self):
69 """Return the list of required services for DBusTestCase."""
70 services = super(DBusTestCase, self).required_services()
71 services.extend([DBusRunner])
72 return services
73
74 @defer.inlineCallbacks
75 def setUp(self):
76 """Setup the infrastructure fo the test (dbus service)."""
77 # Class 'BaseTestCase' has no 'setUp' member
78 # pylint: disable=E1101
79 # dbus modules will be imported by the decorator
80 # pylint: disable=E0602
81 yield super(DBusTestCase, self).setUp()
82
83 # We need to ensure DBUS_SESSION_BUS_ADDRESS is private here
84 from urllib import unquote
85 bus_address = os.environ.get('DBUS_SESSION_BUS_ADDRESS', None)
86 if os.path.dirname(unquote(bus_address.split(',')[0].split('=')[1])) \
87 != os.path.dirname(os.getcwd()):
88 raise InvalidSessionBus('DBUS_SESSION_BUS_ADDRES is wrong.')
89
90 # Set up the main loop and bus connection
91 self.loop = DBusGMainLoop(set_as_default=True)
92 self.bus = dbus.bus.BusConnection(address_or_type=bus_address,
93 mainloop=self.loop)
94
95 # Monkeypatch the dbus.SessionBus/SystemBus methods, to ensure we
96 # always point at our own private bus instance.
97 self.patch(dbus, 'SessionBus', lambda: self.bus)
98 self.patch(dbus, 'SystemBus', lambda: self.bus)
99
100 # Check that we are on the correct bus for real
101# Disable this for now, because our tests are extremely broken :(
102# bus_names = self.bus.list_names()
103# if len(bus_names) > 2:
104# raise InvalidSessionBus('Too many bus connections: %s (%r)' %
105# (len(bus_names), bus_names))
106
107 # monkeypatch busName.__del__ to avoid errors on gc
108 # we take care of releasing the name in shutdown
109 service.BusName.__del__ = lambda _: None
110 yield self.bus.set_exit_on_disconnect(False)
111 self.signal_receivers = set()
112
113 @defer.inlineCallbacks
114 def tearDown(self):
115 """Cleanup the test."""
116 yield self.bus.flush()
117 yield self.bus.close()
118 yield super(DBusTestCase, self).tearDown()
0119
=== added directory 'ubuntuone/devtools/testing'
=== added file 'ubuntuone/devtools/testing/__init__.py'
--- ubuntuone/devtools/testing/__init__.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/testing/__init__.py 2012-01-04 20:35:34 +0000
@@ -0,0 +1,1 @@
1"""Testing helpers."""
02
=== added file 'ubuntuone/devtools/testing/txcheck.py'
--- ubuntuone/devtools/testing/txcheck.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/devtools/testing/txcheck.py 2012-01-04 20:35:34 +0000
@@ -0,0 +1,366 @@
1# -*- coding: utf-8 -*-
2
3# Author: Tim Cole <tim.cole@canonical.com>
4#
5# Copyright 2011 Canonical Ltd.
6#
7# This program is free software: you can redistribute it and/or modify it
8# under the terms of the GNU General Public License version 3, as published
9# by the Free Software Foundation.
10#
11# This program is distributed in the hope that it will be useful, but
12# WITHOUT ANY WARRANTY; without even the implied warranties of
13# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
14# PURPOSE. See the GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License along
17# with this program. If not, see <http://www.gnu.org/licenses/>.
18
19"""Utilities for performing correctness checks."""
20
21import sys
22import ast
23from inspect import getsource
24from textwrap import dedent
25from itertools import takewhile
26from unittest import TestCase, TestSuite, TestResult
27
28from twisted.trial.unittest import TestCase as TwistedTestCase
29
30
31def type_to_name(type_obj):
32 """Return a name for a type."""
33 package_name = getattr(type_obj, '__module__', None)
34 if package_name:
35 return "%s.%s" % (package_name, type_obj.__name__)
36 else:
37 return type_obj.__name__
38
39
40class Problem(AssertionError):
41 """An object representing a problem in a method."""
42
43 def __init__(self, method, test_class, ancestor_class):
44 """Initialize an instance."""
45 super(Problem, self).__init__()
46 self.method = method
47 self.test_class = test_class
48 self.ancestor_class = ancestor_class
49
50 def __eq__(self, other):
51 """Test equality."""
52 return type(self) == type(other) and self.__dict__ == other.__dict__
53
54 def __ne__(self, other):
55 """Test inequality."""
56 return not (self == other)
57
58 def __hash__(self):
59 """Return hash."""
60 member_hash = 0
61 for (key, value) in self.__dict__.iteritems():
62 member_hash ^= hash(key) ^ hash(value)
63 return hash(type(self)) ^ member_hash
64
65 def __str__(self):
66 """Return a friendlier representation."""
67 if self.ancestor_class != self.test_class:
68 method_string = ("%s in ancestor method %s.%s" %
69 (type_to_name(self.test_class),
70 type_to_name(self.ancestor_class),
71 self.method))
72 else:
73 method_string = ("%s.%s" %
74 (type_to_name(self.test_class), self.method))
75 return ("%s for %s" % (type(self).__name__, method_string))
76
77 def __repr__(self):
78 """Return representation string."""
79 return "<%s %r>" % (type(self), self.__dict__)
80
81
82class MethodShadowed(Problem):
83 """Problem when trial's run method is shadowed."""
84
85
86class SuperResultDiscarded(Problem):
87 """Problem when callback chains are broken."""
88
89
90class SuperNotCalled(Problem):
91 """Problem when super isn't called."""
92
93
94class MissingInlineCallbacks(Problem):
95 """Problem when the inlineCallbacks decorator is missing."""
96
97
98class MissingReturnValue(Problem):
99 """Problem when there's no return value."""
100
101
102def match_type(expected_type):
103 """Return predicate matching nodes of given type."""
104 return lambda node: isinstance(node, expected_type)
105
106
107def match_equal(expected_value):
108 """Return predicate matching nodes equaling the given value."""
109 return lambda node: expected_value == node
110
111
112def match_in(expected_values):
113 """Return predicate matching node if in collection of expected values."""
114 return lambda node: node in expected_values
115
116
117def match_not_none():
118 """Returns a predicate matching nodes which are not None."""
119 return lambda node: node is not None
120
121
122def match_any(*subtests):
123 """Return short-circuiting predicate matching any given subpredicate."""
124 if len(subtests) == 1:
125 return subtests[0]
126 else:
127
128 def test(node):
129 """Try each subtest until we find one that passes."""
130 for subtest in subtests:
131 if subtest(node):
132 return True
133 return False
134
135 return test
136
137
138def match_all(*subtests):
139 """Return short-circuiting predicate matching all given subpredicates."""
140 if len(subtests) == 1:
141 return subtests[0]
142 else:
143
144 def test(node):
145 """Try each subtest until we find one that fails."""
146 for subtest in subtests:
147 if not subtest(node):
148 return False
149 return True
150
151 return test
152
153
154def match_attr(attr_name, *tests):
155 """Return predicate matching subpredicates against an attribute value."""
156 return lambda node: match_all(*tests)(getattr(node, attr_name))
157
158
159def match_path(initial_test, *components):
160 """Return predicate which recurses into the tree via given attributes."""
161 components = list(components)
162 components.reverse()
163 test = lambda node: True
164 for component in components:
165 attr_name = component[0]
166 subtests = component[1:]
167 test = match_attr(attr_name, match_all(match_all(*subtests), test))
168 return match_all(initial_test, test)
169
170
171def match_child(*tests):
172 """Return predicate which tests any child."""
173 subtest = match_all(*tests)
174
175 def test(node):
176 """Try each child until we find one that matches."""
177 for child in ast.iter_child_nodes(node):
178 if subtest(child):
179 return True
180 return False
181
182 return test
183
184
185def match_descendant(subtest, prune):
186 """Return predicate which tests a node and any descendants."""
187
188 def test(node):
189 """Recursively (breadth-first) search for a matching node."""
190 for child in ast.iter_child_nodes(node):
191 if prune(child):
192 continue
193 if subtest(child) or test(child):
194 return True
195 return False
196
197 return test
198
199
200def matches(node, *tests):
201 """Convenience function to try predicates on a node."""
202 return match_all(*tests)(node)
203
204
205def any_matches(nodes, *tests):
206 """Convenience function to try predicates on any of a sequence of nodes."""
207 test = match_all(*tests)
208 for node in nodes:
209 if test(node):
210 return True
211 return False
212
213
214def iter_matching_child_nodes(node, *tests):
215 """Yields every matching child node."""
216 test = match_all(*tests)
217 for child in ast.iter_child_nodes(node):
218 if test(child):
219 yield child
220
221
222SETUP_FUNCTION_NAMES = ('setUp', 'tearDown')
223SETUP_FUNCTION = match_path(match_type(ast.FunctionDef),
224 ('name', match_in(SETUP_FUNCTION_NAMES)))
225
226SUPER = match_path(match_type(ast.Call),
227 ('func', match_type(ast.Attribute)),
228 ('value', match_type(ast.Call)),
229 ('func', match_type(ast.Name)),
230 ('id', match_equal("super")))
231
232BARE_SUPER = match_path(match_type(ast.Expr),
233 ('value', SUPER))
234
235YIELD = match_type(ast.Yield)
236
237INLINE_CALLBACKS_DECORATOR = \
238 match_any(match_path(match_type(ast.Attribute),
239 ('attr', match_equal('inlineCallbacks'))),
240 match_path(match_type(ast.Name),
241 ('id', match_equal('inlineCallbacks'))))
242
243RETURN_VALUE = \
244 match_path(match_type(ast.Return),
245 ('value', match_not_none()))
246
247DEFS = match_any(match_type(ast.ClassDef),
248 match_type(ast.FunctionDef))
249
250
251def find_problems(class_to_check):
252 """Check twisted test setup in a given test class."""
253 mro = class_to_check.__mro__
254 if TwistedTestCase not in mro:
255 return set()
256
257 problems = set()
258
259 ancestry = takewhile(lambda c: c != TwistedTestCase, mro)
260 for ancestor_class in ancestry:
261 if 'run' in ancestor_class.__dict__:
262 problem = MethodShadowed(method='run',
263 test_class=class_to_check,
264 ancestor_class=ancestor_class)
265 problems.add(problem)
266
267 source = dedent(getsource(ancestor_class))
268 tree = ast.parse(source)
269 # the top level of the tree is a Module
270 class_node = tree.body[0]
271
272 # Check setUp/tearDown
273 for def_node in iter_matching_child_nodes(class_node, SETUP_FUNCTION):
274 if matches(def_node, match_child(BARE_SUPER)):
275 # Superclass method called, but its result wasn't used
276 problem = SuperResultDiscarded(method=def_node.name,
277 test_class=class_to_check,
278 ancestor_class=ancestor_class)
279 problems.add(problem)
280 if not matches(def_node, match_descendant(SUPER, DEFS)):
281 # The call to the overridden superclass method is missing
282 problem = SuperNotCalled(method=def_node.name,
283 test_class=class_to_check,
284 ancestor_class=ancestor_class)
285 problems.add(problem)
286
287 decorators = def_node.decorator_list
288
289 if matches(def_node, match_descendant(YIELD, DEFS)):
290 # Yield was used, making this a generator
291 if not any_matches(decorators, INLINE_CALLBACKS_DECORATOR):
292 # ...but the inlineCallbacks decorator is missing
293 problem = MissingInlineCallbacks(
294 method=def_node.name,
295 test_class=class_to_check,
296 ancestor_class=ancestor_class)
297 problems.add(problem)
298 else:
299 if not matches(def_node, match_descendant(RETURN_VALUE, DEFS)):
300 # The function fails to return a deferred
301 problem = MissingReturnValue(
302 method=def_node.name,
303 test_class=class_to_check,
304 ancestor_class=ancestor_class)
305 problems.add(problem)
306
307 return problems
308
309
310def get_test_classes(suite):
311 """Return all the unique test classes involved in a suite."""
312 classes = set()
313
314 def find_classes(suite_or_test):
315 """Recursively find all the test classes."""
316 if isinstance(suite_or_test, TestSuite):
317 for subtest in suite_or_test:
318 find_classes(subtest)
319 else:
320 classes.add(type(suite_or_test))
321
322 find_classes(suite)
323
324 return classes
325
326
327def make_check_testcase(tests):
328 """Make TestCase which checks the given twisted tests."""
329
330 class TXCheckTest(TestCase):
331 """Test case which checks the test classes for problems."""
332
333 def runTest(self): # pylint: disable=C0103
334 """Do nothing."""
335
336 def run(self, result=None):
337 """Check all the test classes for problems."""
338 if result is None:
339 result = TestResult()
340
341 test_classes = set()
342
343 for test_object in tests:
344 test_classes |= get_test_classes(test_object)
345
346 for test_class in test_classes:
347 problems = find_problems(test_class)
348 for problem in problems:
349 try:
350 raise problem
351 except Problem:
352 result.addFailure(self, sys.exc_info())
353
354 return TXCheckTest()
355
356
357class TXCheckSuite(TestSuite):
358 """Test suite which checks twisted tests."""
359
360 def __init__(self, tests=()):
361 """Initialize with the given tests, and add a special test."""
362
363 tests = list(tests)
364 tests.insert(0, make_check_testcase(self))
365
366 super(TXCheckSuite, self).__init__(tests)

Subscribers

People subscribed via source and target branches

to all changes: