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
1=== modified file 'PKG-INFO'
2--- PKG-INFO 2011-09-13 14:48:46 +0000
3+++ PKG-INFO 2012-01-04 20:35:34 +0000
4@@ -1,6 +1,6 @@
5 Metadata-Version: 1.0
6 Name: ubuntuone-dev-tools
7-Version: 0.2.0
8+Version: 2.99.1
9 Summary: Ubuntu One development tools and utilities
10 Home-page: http://launchpad.net/ubuntuone-dev-tools
11 Author: UNKNOWN
12
13=== modified file 'bin/u1lint'
14--- bin/u1lint 2011-09-13 14:48:46 +0000
15+++ bin/u1lint 2012-01-04 20:35:34 +0000
16@@ -4,7 +4,7 @@
17 #
18 # Author: Rodney Dawes <rodney.dawes@canonical.com>
19 #
20-# Copyright 2009-2010 Canonical Ltd.
21+# Copyright 2009-2011 Canonical Ltd.
22 #
23 # This program is free software: you can redistribute it and/or modify it
24 # under the terms of the GNU General Public License version 3, as published
25@@ -25,7 +25,7 @@
26 import subprocess
27 import sys
28
29-from xdg.BaseDirectory import xdg_data_dirs
30+from dirspec.basedir import xdg_data_dirs
31
32 SRCDIR = os.environ.get('SRCDIR', os.getcwd())
33
34@@ -33,7 +33,7 @@
35 class InvalidSetupException(Exception):
36 """Raised when the env is not correctly setup."""
37
38-
39+
40 def find_python_installation_path():
41 """Return the path where python was installed."""
42 assert(sys.platform == 'win32')
43@@ -98,7 +98,8 @@
44 # the default is to assume that the script is executable and that it
45 # can be found in the path
46 return [script, ]
47-
48+
49+
50 def find_pylintrc():
51 """Return the first pylintrc found."""
52 # Use the pylintrc in the source tree if there is one
53@@ -114,8 +115,10 @@
54 return full_name
55 return None
56
57+
58 PYLINTRC = find_pylintrc()
59
60+
61 def _read_pylintrc_ignored():
62 """Get the ignored files list from pylintrc"""
63 try:
64@@ -129,6 +132,7 @@
65 return []
66 # pylint: enable=E1103
67
68+
69 def _group_lines_by_file(data):
70 """Format file:line:message output as lines grouped by file."""
71 did_fail = False
72@@ -156,6 +160,7 @@
73
74 return (did_fail, "\n".join(outputs))
75
76+
77 def _find_files():
78 """Find all Python files under the current tree."""
79 pyfiles = []
80@@ -199,7 +204,7 @@
81 else:
82 pylint_args = get_subprocess_start_info('pylint')
83 # append the extra args to the start info
84- pylint_args.extend(['--output-format=parseable',
85+ pylint_args.extend(['--output-format=parseable',
86 '--include-ids=yes'])
87 if PYLINTRC:
88 pylint_args.append("--rcfile=" + PYLINTRC)
89
90=== modified file 'bin/u1trial'
91--- bin/u1trial 2011-09-13 14:48:46 +0000
92+++ bin/u1trial 2012-01-04 20:35:34 +0000
93@@ -4,7 +4,7 @@
94 #
95 # Author: Rodney Dawes <rodney.dawes@canonical.com>
96 #
97-# Copyright 2009-2010 Canonical Ltd.
98+# Copyright 2009-2011 Canonical Ltd.
99 #
100 # This program is free software: you can redistribute it and/or modify it
101 # under the terms of the GNU General Public License version 3, as published
102@@ -28,11 +28,14 @@
103 import sys
104 import unittest
105
106+from twisted.python.usage import UsageError
107+from twisted.scripts import trial
108 from twisted.trial.runner import TrialRunner
109
110-
111 sys.path.insert(0, os.path.abspath("."))
112
113+from ubuntuone.devtools.testing.txcheck import TXCheckSuite
114+
115
116 def _is_in_ignored_path(testcase, paths):
117 """Return if the testcase is in one of the ignored paths."""
118@@ -45,12 +48,10 @@
119 class TestRunner(TrialRunner):
120 """The test runner implementation."""
121
122- def __init__(self, force_gc=False):
123- from twisted.trial.reporter import TreeReporter
124-
125+ def __init__(self, config=None):
126 # set $HOME to the _trial_temp dir, to avoid breaking user files
127 trial_temp_dir = os.environ.get('TRIAL_TEMP_DIR', os.getcwd())
128- homedir = os.path.join(trial_temp_dir, '_trial_temp')
129+ homedir = os.path.join(trial_temp_dir, config['temp-directory'])
130 os.environ['HOME'] = homedir
131
132 # setup $XDG_*_HOME variables and create the directories
133@@ -71,12 +72,29 @@
134 # setup the ROOTDIR env var
135 os.environ['ROOTDIR'] = os.getcwd()
136
137+ # Need an attribute for tempdir so we can use it later
138 self.tempdir = homedir
139- working_dir = os.path.join(self.tempdir, 'tmp')
140- super(TestRunner, self).__init__(reporterFactory=TreeReporter,
141- realTimeErrors=True,
142- workingDirectory=working_dir,
143- forceGarbageCollection=force_gc)
144+ working_dir = os.path.join(self.tempdir, 'trial')
145+
146+ # Handle running trial in debug or dry-run mode
147+ mode = None
148+ if config['debug']:
149+ mode = TrialRunner.DEBUG
150+ if config['dry-run']:
151+ mode = TrialRunner.DRY_RUN
152+
153+ # Hook up to the parent test runner
154+ super(TestRunner, self).__init__(
155+ reporterFactory=config['reporter'],
156+ mode=mode,
157+ profile=config['profile'],
158+ logfile=config['logfile'],
159+ tracebackFormat=config['tbformat'],
160+ realTimeErrors=config['rterrors'],
161+ uncleanWarnings=config['unclean-warnings'],
162+ workingDirectory=working_dir,
163+ forceGarbageCollection=config['force-gc'])
164+
165 self.required_services = []
166 self.source_files = []
167
168@@ -112,23 +130,11 @@
169 def _collect_tests(self, path, test_pattern, ignored_modules,
170 ignored_paths):
171 """Return the set of unittests."""
172- suite = unittest.TestSuite()
173+ suite = TXCheckSuite()
174 if test_pattern:
175 pattern = re.compile('.*%s.*' % test_pattern)
176 else:
177 pattern = None
178-
179- # get the ignored modules/tests
180- if ignored_modules:
181- ignored_modules = map(str.strip, ignored_modules.split(','))
182- else:
183- ignored_modules = []
184-
185- # get the ignored paths
186- if ignored_paths:
187- ignored_paths = map(str.strip, ignored_paths.split(','))
188- else:
189- ignored_paths = []
190
191 # Disable this lint warning as we need to access _tests in the
192 # test suites, to collect the tests
193@@ -169,100 +175,150 @@
194 suite.addTests(module_suite)
195 return suite
196
197- # pylint: disable=E0202
198- def run(self, args, options=None):
199+ def get_suite(self, config):
200+ """Get the test suite to use."""
201+ suite = unittest.TestSuite()
202+ for path in config['tests']:
203+ suite.addTest(self._collect_tests(path, config['test'],
204+ config['ignore-modules'],
205+ config['ignore-paths']))
206+ if config['loop']:
207+ old_suite = suite
208+ suite = unittest.TestSuite()
209+ for _ in xrange(config['loop']):
210+ suite.addTest(old_suite)
211+
212+ return suite
213+
214+ # pylint: disable=C0103
215+ def _runWithoutDecoration(self, test):
216 """run the tests."""
217- success = 0
218+ result = None
219 running_services = []
220- if options.coverage:
221- coverage.erase()
222- coverage.start()
223
224 try:
225- suite = unittest.TestSuite()
226- for path in args:
227- print "Adding path"
228- suite.addTest(self._collect_tests(path, options.test,
229- options.ignored_modules,
230- options.ignored_paths))
231- if options.loops:
232- old_suite = suite
233- suite = unittest.TestSuite()
234- for _ in xrange(options.loops):
235- suite.addTest(old_suite)
236-
237 # Start any required services
238 for service in self.required_services:
239 runner = service()
240 runner.start_service(tempdir=self.tempdir)
241 running_services.append(runner)
242
243- result = super(TestRunner, self).run(suite)
244- success = result.wasSuccessful()
245+ result = super(TestRunner, self)._runWithoutDecoration(test)
246 finally:
247 # Stop all the running services
248 for runner in running_services:
249 runner.stop_service()
250
251- if options.coverage:
252- coverage.stop()
253- coverage.report(self.source_files, ignore_errors=True,
254- show_missing=False)
255-
256- if not success:
257- sys.exit(1)
258- else:
259- sys.exit(0)
260+ return result
261+
262+
263+class Options(trial.Options):
264+ """Class for options handling."""
265+
266+ optFlags = [["coverage", "c"],
267+ ["gui", None],
268+ ["help-reactors", None],
269+ ]
270+
271+ optParameters = [["test", "t", None],
272+ ["loop", None, 1],
273+ ["ignore-modules", "i", ""],
274+ ["ignore-paths", "p", ""],
275+ ["reactor", "r", "glib"],
276+ ]
277+
278+ def __init__(self):
279+ self['tests'] = set()
280+ super(Options, self).__init__()
281+ self['rterrors'] = True
282+
283+ def opt_coverage(self):
284+ """Generate a coverage report for the run tests"""
285+ self['coverage'] = True
286+
287+ def opt_gui(self):
288+ """Use the GUI mode of some reactors"""
289+ self['gui'] = True
290+
291+ def opt_help_reactors(self):
292+ """Help on available reactors for use with tests"""
293+ synopsis = ('')
294+ print synopsis
295+ print 'Need to get list of reactors and print them here.'
296+ print
297+ sys.exit(0)
298+
299+ def opt_test(self, option):
300+ """Run specific tests, e.g: className.methodName"""
301+ self['test'] = option
302+
303+ def opt_loop(self, option):
304+ """Loop tests the specified number of times."""
305+ try:
306+ self['loop'] = long(option)
307+ except ValueError:
308+ raise UsageError('A positive integer value must be specified.')
309+
310+ def opt_ignore_modules(self, option):
311+ """Comma-separate list of test modules to ignore,
312+ e.g: test_gtk.py, test_account.py
313+ """
314+ self['ignore-modules'] = map(str.strip, option.split(','))
315+
316+ def opt_ignore_paths(self, option):
317+ """Comma-separated list of relative paths to ignore,
318+ e.g: tests/platform/windows, tests/platform/macosx
319+ """
320+ self['ignore-paths'] = map(str.strip, option.split(','))
321+
322+ def opt_reactor(self, option):
323+ """Which reactor to use (see --help-reactors for a list
324+ of possibilities)
325+ """
326+ self['reactor'] = option
327+ opt_r = opt_reactor
328
329
330 def main():
331 """Do the deed."""
332- from optparse import OptionParser
333- usage = '%prog [options] path'
334- parser = OptionParser(usage=usage)
335- parser.add_option("-t", "--test", dest="test",
336- help = "run specific tests, e.g: className.methodName")
337- parser.add_option("-l", "--loop", dest="loops", type="int", default=1,
338- help = "loop selected tests LOOPS number of times",
339- metavar="LOOPS")
340- parser.add_option("-c", "--coverage", action="store_true", dest="coverage",
341- help="print a coverage report when finished")
342- parser.add_option("-i", "--ignored-modules", dest="ignored_modules",
343- default=None, help="comma-separated test moodules "
344- + "to ignore, e.g: test_gtk.py, test_account.py")
345- parser.add_option("-p", "--ignore-paths", dest="ignored_paths",
346- default=None, help="comma-separated relative "
347- + "paths to ignore. "
348- + "e.g: tests/platform/windows, tests/platform/macosx")
349- parser.add_option("--force-gc", action="store_true", dest="force_gc",
350- default=False, help="Run gc.collect() before and after "
351- "each test case.")
352- parser.add_option("--reactor", type="string", dest="reactor",
353- default='glib',
354- help="Run the tests using the specified reactor.",
355- metavar="REACTOR")
356- parser.add_option("--gui", action="store_true", dest="use_gui",
357- help="Use the GUI mode of some reactors.")
358- (options, args) = parser.parse_args()
359- if not args:
360- parser.print_help()
361- sys.exit(2)
362+ if len(sys.argv) == 1:
363+ sys.argv.append('--help')
364+
365+ config = Options()
366+ config.parseOptions()
367
368 try:
369- reactor_name = 'ubuntuone.devtools.reactors.%s' % options.reactor
370+ reactor_name = 'ubuntuone.devtools.reactors.%s' % config['reactor']
371 reactor = __import__(reactor_name, None, None, [''])
372 except ImportError:
373 print 'The specified reactor is not supported.'
374 sys.exit(1)
375 else:
376 try:
377- reactor.install(options=options)
378+ reactor.install(options=config)
379 except ImportError:
380 print('The Python package providing the requested reactor is not '
381 'installed. You can find it here: %s' % reactor.REACTOR_URL)
382 raise
383
384- TestRunner(force_gc=options.force_gc).run(args, options)
385+ trial_runner = TestRunner(config=config)
386+ suite = trial_runner.get_suite(config)
387+
388+ if config['coverage']:
389+ coverage.erase()
390+ coverage.start()
391+
392+ if config['until-failure']:
393+ result = trial_runner.runUntilFailure(suite)
394+ else:
395+ result = trial_runner.run(suite)
396+
397+ if config['coverage']:
398+ coverage.stop()
399+ coverage.report(trial_runner.source_files, ignore_errors=True,
400+ show_missing=False)
401+
402+ sys.exit(not result.wasSuccessful())
403
404
405 if __name__ == '__main__':
406
407=== modified file 'debian/changelog'
408--- debian/changelog 2011-09-15 20:52:05 +0000
409+++ debian/changelog 2012-01-04 20:35:34 +0000
410@@ -1,3 +1,23 @@
411+ubuntuone-dev-tools (2.99.1-0ubuntu1) precise; urgency=low
412+
413+ * New upstream release.
414+
415+ -- Rodney Dawes <rodney.dawes@ubuntu.com> Wed, 04 Jan 2012 13:12:03 -0500
416+
417+ubuntuone-dev-tools (2.99.0-0ubuntu1) precise; urgency=low
418+
419+ * New upstream release.
420+ - Depends on dirspec now (LP: #888619)
421+ - Pass options through to trial (LP: #890786)
422+ * debian/control:
423+ - Move python-gobject to Suggests
424+ - Suggest python-qt4reactor
425+ * debian/rules:
426+ - Convert to pure dh
427+ - Run unit tests during build
428+
429+ -- Rodney Dawes <rodney.dawes@ubuntu.com> Thu, 22 Dec 2011 16:33:22 -0500
430+
431 ubuntuone-dev-tools (0.2.0-0ubuntu1) oneiric; urgency=low
432
433 * New upstream release.
434
435=== modified file 'debian/control'
436--- debian/control 2011-09-15 20:52:05 +0000
437+++ debian/control 2012-01-04 20:35:34 +0000
438@@ -3,27 +3,29 @@
439 XSBC-Original-Maintainer: Rodney Dawes <rodney.dawes@ubuntu.com>
440 Section: python
441 Priority: optional
442-Standards-Version: 3.9.1
443+Standards-Version: 3.9.2
444+X-Python-Version: >= 2.6
445 Build-Depends-Indep:
446 dbus,
447 pep8,
448 pylint (>= 0.21.0),
449 python-coverage,
450 python-dbus,
451+ python-dirspec (>= 2.99.0),
452 python-gobject,
453 python-qt4,
454 python-setuptools,
455- python-support,
456- python-twisted-core,
457- python-xdg
458-Build-Depends: cdbs (>= 0.4.43), debhelper (>= 7.0.17), python-all
459+ python-twisted-core
460+Build-Depends: debhelper (>= 7.0.50),
461+ python-all (>= 2.6.6-3)
462 Homepage: http://launchpad.net/ubuntuone-dev-tools
463
464 Package: python-ubuntuone-devtools
465 Architecture: all
466 Depends: ${python:Depends}, ${misc:Depends},
467 dbus,
468- python-dbus
469+ python-dbus,
470+ python-dirspec (>= 2.99.0)
471 Description: Ubuntu One development tools - Python modules
472 Ubuntu One development tools provides scripts, test cases, and other
473 utilities for developing Python projects which need more integration
474@@ -35,10 +37,9 @@
475 Depends: ${python:Depends}, ${misc:Depends}, python,
476 pylint (>= 0.21.0) | pyflakes,
477 python-coverage,
478- python-gobject,
479 python-twisted-core,
480- python-ubuntuone-devtools (= ${binary:Version}),
481- python-xdg
482+ python-ubuntuone-devtools (= ${binary:Version})
483+Suggests: python-gobject, python-qt4reactor
484 Description: Ubuntu One development tools
485 Ubuntu One development tools provides scripts, test cases, and other
486 utilities for developing Python projects which need more integration
487
488=== removed file 'debian/pycompat'
489--- debian/pycompat 2010-08-02 13:45:54 +0000
490+++ debian/pycompat 1970-01-01 00:00:00 +0000
491@@ -1,1 +0,0 @@
492-2
493
494=== modified file 'debian/rules'
495--- debian/rules 2010-11-30 18:59:50 +0000
496+++ debian/rules 2012-01-04 20:35:34 +0000
497@@ -1,13 +1,8 @@
498 #!/usr/bin/make -f
499
500-include /usr/share/cdbs/1/rules/debhelper.mk
501-DEB_PYTHON_SYSTEM=pysupport
502-include /usr/share/cdbs/1/class/python-distutils.mk
503-
504-common-build-arch common-build-indep:: debian/stamp-check
505-debian/stamp-check:
506- cd $(DEB_SRCDIR) && ./run-tests
507- touch $@
508-
509-makefile-clean::
510- rm -f debian/stamp-check
511+%:
512+ dh --with python2 $@
513+
514+override_dh_auto_test:
515+ ./run-tests
516+
517
518=== modified file 'run-tests'
519--- run-tests 2011-09-13 14:48:46 +0000
520+++ run-tests 2012-01-04 20:35:34 +0000
521@@ -1,7 +1,7 @@
522 #!/bin/bash
523 # Author: Natalia Bidart <natalia.bidart@canonical.com>
524 #
525-# Copyright 2010 Canonical Ltd.
526+# Copyright 2010-2011 Canonical Ltd.
527 #
528 # This program is free software: you can redistribute it and/or modify it
529 # under the terms of the GNU General Public License version 3, as published
530@@ -18,7 +18,8 @@
531
532 bin/u1trial -c ubuntuone
533 bin/u1trial --reactor=twisted ubuntuone
534+echo "Running style checks..."
535 bin/u1lint
536-pep8 --repeat .
537+pep8 --repeat . bin/*
538 rm -rf _trial_temp
539 rm -rf .coverage
540
541=== modified file 'setup.py'
542--- setup.py 2011-09-13 14:48:46 +0000
543+++ setup.py 2012-01-04 20:35:34 +0000
544@@ -21,7 +21,7 @@
545 from distutils.core import setup, Command
546
547 PACKAGE = 'ubuntuone-dev-tools'
548-VERSION = '0.2.0'
549+VERSION = '2.99.1'
550
551 U1LINT = 'bin/u1lint'
552
553@@ -51,7 +51,9 @@
554 packages=['ubuntuone',
555 'ubuntuone.devtools',
556 'ubuntuone.devtools.reactors',
557- 'ubuntuone.devtools.services'],
558+ 'ubuntuone.devtools.services',
559+ 'ubuntuone.devtools.testing',
560+ 'ubuntuone.devtools.testcases'],
561 extra_path='ubuntuone-dev-tools',
562 scripts=['bin/u1lint',
563 'bin/u1trial',
564
565=== modified file 'ubuntuone/devtools/reactors/glib.py'
566--- ubuntuone/devtools/reactors/glib.py 2011-09-13 14:48:46 +0000
567+++ ubuntuone/devtools/reactors/glib.py 2012-01-04 20:35:34 +0000
568@@ -17,7 +17,7 @@
569 def install(options=None):
570 """Install the reactor and parse any options we might need."""
571 reactor_name = None
572- if options is not None and options.use_gui:
573+ if options is not None and options['gui']:
574 reactor_name = 'twisted.internet.gtk2reactor'
575 else:
576 reactor_name = 'twisted.internet.glib2reactor'
577
578=== modified file 'ubuntuone/devtools/reactors/qt4.py'
579--- ubuntuone/devtools/reactors/qt4.py 2011-09-13 14:48:46 +0000
580+++ ubuntuone/devtools/reactors/qt4.py 2012-01-04 20:35:34 +0000
581@@ -22,12 +22,19 @@
582
583 def install(options=None):
584 """Install the reactor and parse any options we might need."""
585- if options is not None and options.use_gui:
586+ if options is not None and options['gui']:
587 from PyQt4.QtGui import QApplication
588 # We must assign this to a variable, or we will get crashes in Qt
589 # pylint: disable=W0612
590 app = QApplication(sys.argv)
591 # pylint: enable=W0612
592
593- qt4reactor = __import__('qtreactor.qt4reactor', None, None, [''])
594+ try:
595+ qt4reactor = __import__('qt4reactor', None, None, [''])
596+ except ImportError:
597+ # Leave this import in place for a couple of weeks
598+ # until all the devs are using the packaged qt4reactor
599+ # (nessita, 11-11-11)
600+ qt4reactor = __import__('qtreactor.qt4reactor', None, None, [''])
601+
602 qt4reactor.install()
603
604=== modified file 'ubuntuone/devtools/services/dbus.py'
605--- ubuntuone/devtools/services/dbus.py 2011-09-13 14:48:46 +0000
606+++ ubuntuone/devtools/services/dbus.py 2012-01-04 20:35:34 +0000
607@@ -20,9 +20,9 @@
608 import signal
609 import subprocess
610
611+from dirspec.basedir import load_data_paths
612 from distutils.spawn import find_executable
613 from urllib import quote
614-from xdg.BaseDirectory import load_data_paths
615
616
617 class DBusLaunchError(Exception):
618
619=== modified file 'ubuntuone/devtools/testcase.py'
620--- ubuntuone/devtools/testcase.py 2011-09-13 14:48:46 +0000
621+++ ubuntuone/devtools/testcase.py 2012-01-04 20:35:34 +0000
622@@ -1,8 +1,6 @@
623 # -*- coding: utf-8 -*-
624-
625-# Author: Guillermo Gonzalez <guillermo.gonzalez@canonical.com>
626 #
627-# Copyright 2009-2010 Canonical Ltd.
628+# Copyright 2011 Canonical Ltd.
629 #
630 # This program is free software: you can redistribute it and/or modify it
631 # under the terms of the GNU General Public License version 3, as published
632@@ -15,245 +13,13 @@
633 #
634 # You should have received a copy of the GNU General Public License along
635 # with this program. If not, see <http://www.gnu.org/licenses/>.
636-
637-"""Base tests cases and test utilities."""
638-
639-from __future__ import with_statement
640-
641-import contextlib
642-import os
643-import shutil
644-import sys
645-
646-from functools import wraps
647-
648-from twisted.internet import defer
649-from twisted.trial.unittest import TestCase, SkipTest
650-
651-# DBusRunner for DBusTestCase using tests
652-from ubuntuone.devtools.services.dbus import DBusRunner
653-
654-
655-# pylint: disable=F0401,C0103
656-try:
657- import dbus
658-except ImportError:
659- dbus = None
660-
661-try:
662- import dbus.service as service
663-except ImportError:
664- service = None
665-
666-try:
667- from dbus.mainloop.glib import DBusGMainLoop
668-except ImportError:
669- DBusGMainLoop = None
670-
671-
672-# pylint: enable=F0401,C0103
673-@contextlib.contextmanager
674-def environ(env_var, new_value):
675- """context manager to replace/add an environ value"""
676- old_value = os.environ.get(env_var, None)
677- os.environ[env_var] = new_value
678- yield
679- if old_value is None:
680- os.environ.pop(env_var)
681- else:
682- os.environ[env_var] = old_value
683-
684-
685-def _id(obj):
686- """Return the obj calling the funct."""
687- return obj
688-
689-
690-# pylint: disable=C0103
691-def skipTest(reason):
692- """Unconditionally skip a test."""
693-
694- def decorator(test_item):
695- """Decorate the test so that it is skipped."""
696- if not (isinstance(test_item, type) and\
697- issubclass(test_item, TestCase)):
698-
699- @wraps(test_item)
700- def skip_wrapper(*args, **kwargs):
701- """Skip a test method raising an exception."""
702- raise SkipTest(reason)
703- test_item = skip_wrapper
704-
705- # tell twisted.trial.unittest to skip the test, pylint will complain
706- # since it thinks we are redefining a name out of the scope
707- # pylint: disable=W0621,W0612
708- test_item.skip = reason
709- # pylint: enable=W0621,W0612
710- # because the item was skipped, we will make sure that no
711- # services are started for it
712- if hasattr(test_item, "required_services"):
713- # pylint: disable=W0612
714- test_item.required_services = lambda *args, **kwargs: []
715- # pylint: enable=W0612
716- return test_item
717- return decorator
718-
719-
720-def skipIf(condition, reason):
721- """Skip a test if the condition is true."""
722- if condition:
723- return skipTest(reason)
724- return _id
725-
726-
727-def skipIfOS(current_os, reason):
728- """Skip test for a particular os or lists of them."""
729- if os:
730- if sys.platform in current_os or sys.platform == current_os:
731- return skipTest(reason)
732- return _id
733- return _id
734-
735-
736-def skipIfNotOS(current_os, reason):
737- """Skip test we are not in a particular os."""
738- if os:
739- if sys.platform not in current_os or\
740- sys.platform != current_os:
741- return skipTest(reason)
742- return _id
743- return _id
744-
745-
746-# pylint: enable=C0103
747-
748-
749-class InvalidSessionBus(Exception):
750- """Error when we are connected to the wrong session bus in tests."""
751-
752-
753-class FakeDBusInterface(object):
754- """A fake DBusInterface..."""
755-
756- def shutdown(self, with_restart=False):
757- """...that only knows how to go away"""
758-
759-
760-class BaseTestCase(TestCase):
761- """Base TestCase with helper methods to handle temp dir.
762-
763- This class provides:
764- mktemp(name): helper to create temporary dirs
765- rmtree(path): support read-only shares
766- makedirs(path): support read-only shares
767-
768- """
769-
770- def required_services(self):
771- """Return the list of required services for DBusTestCase."""
772- return []
773-
774- def mktemp(self, name='temp'):
775- """Customized mktemp that accepts an optional name argument."""
776- tempdir = os.path.join(self.tmpdir, name)
777- if os.path.exists(tempdir):
778- self.rmtree(tempdir)
779- self.makedirs(tempdir)
780- return tempdir
781-
782- @property
783- def tmpdir(self):
784- """Default tmpdir: module/class/test_method."""
785- # check if we already generated the root path
786- root_dir = getattr(self, '__root', None)
787- if root_dir:
788- return root_dir
789- max_filename = 32 # some platforms limit lengths of filenames
790- base = os.path.join(self.__class__.__module__[:max_filename],
791- self.__class__.__name__[:max_filename],
792- self._testMethodName[:max_filename])
793- # use _trial_temp dir, it should be os.gwtcwd()
794- # define the root temp dir of the testcase, pylint: disable=W0201
795- self.__root = os.path.join(os.getcwd(), base)
796- return self.__root
797-
798- def rmtree(self, path):
799- """Custom rmtree that handle ro parent(s) and childs."""
800- if not os.path.exists(path):
801- return
802- # change perms to rw, so we can delete the temp dir
803- if path != getattr(self, '__root', None):
804- os.chmod(os.path.dirname(path), 0755)
805- if not os.access(path, os.W_OK):
806- os.chmod(path, 0755)
807- # pylint: disable=W0612
808- for dirpath, dirs, files in os.walk(path):
809- for dirname in dirs:
810- if not os.access(os.path.join(dirpath, dirname), os.W_OK):
811- os.chmod(os.path.join(dirpath, dirname), 0777)
812- shutil.rmtree(path)
813-
814- def makedirs(self, path):
815- """Custom makedirs that handle ro parent."""
816- parent = os.path.dirname(path)
817- if os.path.exists(parent):
818- os.chmod(parent, 0755)
819- os.makedirs(path)
820-
821-
822-@skipIf(dbus is None or service is None or DBusGMainLoop is None,
823- "The test requires dbus.")
824-class DBusTestCase(BaseTestCase):
825- """Test the DBus event handling."""
826-
827- def required_services(self):
828- """Return the list of required services for DBusTestCase."""
829- services = super(DBusTestCase, self).required_services()
830- services.extend([DBusRunner])
831- return services
832-
833- @defer.inlineCallbacks
834- def setUp(self):
835- """Setup the infrastructure fo the test (dbus service)."""
836- # Class 'BaseTestCase' has no 'setUp' member
837- # pylint: disable=E1101
838- # dbus modules will be imported by the decorator
839- # pylint: disable=E0602
840- yield super(DBusTestCase, self).setUp()
841-
842- # We need to ensure DBUS_SESSION_BUS_ADDRESS is private here
843- from urllib import unquote
844- bus_address = os.environ.get('DBUS_SESSION_BUS_ADDRESS', None)
845- if os.path.dirname(unquote(bus_address.split(',')[0].split('=')[1])) \
846- != os.path.dirname(os.getcwd()):
847- raise InvalidSessionBus('DBUS_SESSION_BUS_ADDRES is wrong.')
848-
849- # Set up the main loop and bus connection
850- self.loop = DBusGMainLoop(set_as_default=True)
851- self.bus = dbus.bus.BusConnection(address_or_type=bus_address,
852- mainloop=self.loop)
853-
854- # Monkeypatch the dbus.SessionBus/SystemBus methods, to ensure we
855- # always point at our own private bus instance.
856- self.patch(dbus, 'SessionBus', lambda: self.bus)
857- self.patch(dbus, 'SystemBus', lambda: self.bus)
858-
859- # Check that we are on the correct bus for real
860-# Disable this for now, because our tests are extremely broken :(
861-# bus_names = self.bus.list_names()
862-# if len(bus_names) > 2:
863-# raise InvalidSessionBus('Too many bus connections: %s (%r)' %
864-# (len(bus_names), bus_names))
865-
866- # monkeypatch busName.__del__ to avoid errors on gc
867- # we take care of releasing the name in shutdown
868- service.BusName.__del__ = lambda _: None
869- yield self.bus.set_exit_on_disconnect(False)
870- self.signal_receivers = set()
871-
872- @defer.inlineCallbacks
873- def tearDown(self):
874- """Cleanup the test."""
875- yield self.bus.flush()
876- yield self.bus.close()
877- yield super(DBusTestCase, self).tearDown()
878+"""Maintain old API."""
879+
880+import warnings
881+warnings.warn(
882+ 'Deprecated import path; use ubuntuone.devtools.testcases'
883+ 'instead.', DeprecationWarning, stacklevel=2)
884+
885+# pylint: disable=W0401,W0614
886+from ubuntuone.devtools.testcases import *
887+from ubuntuone.devtools.testcases.dbus import *
888
889=== added directory 'ubuntuone/devtools/testcases'
890=== added file 'ubuntuone/devtools/testcases/__init__.py'
891--- ubuntuone/devtools/testcases/__init__.py 1970-01-01 00:00:00 +0000
892+++ ubuntuone/devtools/testcases/__init__.py 2012-01-04 20:35:34 +0000
893@@ -0,0 +1,166 @@
894+# -*- coding: utf-8 -*-
895+#
896+# Copyright 2009-2011 Canonical Ltd.
897+#
898+# This program is free software: you can redistribute it and/or modify it
899+# under the terms of the GNU General Public License version 3, as published
900+# by the Free Software Foundation.
901+#
902+# This program is distributed in the hope that it will be useful, but
903+# WITHOUT ANY WARRANTY; without even the implied warranties of
904+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
905+# PURPOSE. See the GNU General Public License for more details.
906+#
907+# You should have received a copy of the GNU General Public License along
908+# with this program. If not, see <http://www.gnu.org/licenses/>.
909+
910+"""Base tests cases and test utilities."""
911+
912+from __future__ import with_statement
913+
914+import contextlib
915+import os
916+import shutil
917+import sys
918+
919+from functools import wraps
920+
921+from twisted.trial.unittest import TestCase, SkipTest
922+
923+
924+@contextlib.contextmanager
925+def environ(env_var, new_value):
926+ """context manager to replace/add an environ value"""
927+ old_value = os.environ.get(env_var, None)
928+ os.environ[env_var] = new_value
929+ yield
930+ if old_value is None:
931+ os.environ.pop(env_var)
932+ else:
933+ os.environ[env_var] = old_value
934+
935+
936+def _id(obj):
937+ """Return the obj calling the funct."""
938+ return obj
939+
940+
941+# pylint: disable=C0103
942+def skipTest(reason):
943+ """Unconditionally skip a test."""
944+
945+ def decorator(test_item):
946+ """Decorate the test so that it is skipped."""
947+ if not (isinstance(test_item, type) and\
948+ issubclass(test_item, TestCase)):
949+
950+ @wraps(test_item)
951+ def skip_wrapper(*args, **kwargs):
952+ """Skip a test method raising an exception."""
953+ raise SkipTest(reason)
954+ test_item = skip_wrapper
955+
956+ # tell twisted.trial.unittest to skip the test, pylint will complain
957+ # since it thinks we are redefining a name out of the scope
958+ # pylint: disable=W0621,W0612
959+ test_item.skip = reason
960+ # pylint: enable=W0621,W0612
961+ # because the item was skipped, we will make sure that no
962+ # services are started for it
963+ if hasattr(test_item, "required_services"):
964+ # pylint: disable=W0612
965+ test_item.required_services = lambda *args, **kwargs: []
966+ # pylint: enable=W0612
967+ return test_item
968+ return decorator
969+
970+
971+def skipIf(condition, reason):
972+ """Skip a test if the condition is true."""
973+ if condition:
974+ return skipTest(reason)
975+ return _id
976+
977+
978+def skipIfOS(current_os, reason):
979+ """Skip test for a particular os or lists of them."""
980+ if os:
981+ if sys.platform in current_os or sys.platform == current_os:
982+ return skipTest(reason)
983+ return _id
984+ return _id
985+
986+
987+def skipIfNotOS(current_os, reason):
988+ """Skip test we are not in a particular os."""
989+ if os:
990+ if sys.platform not in current_os or\
991+ sys.platform != current_os:
992+ return skipTest(reason)
993+ return _id
994+ return _id
995+
996+
997+# pylint: enable=C0103
998+
999+
1000+class BaseTestCase(TestCase):
1001+ """Base TestCase with helper methods to handle temp dir.
1002+
1003+ This class provides:
1004+ mktemp(name): helper to create temporary dirs
1005+ rmtree(path): support read-only shares
1006+ makedirs(path): support read-only shares
1007+
1008+ """
1009+
1010+ def required_services(self):
1011+ """Return the list of required services for DBusTestCase."""
1012+ return []
1013+
1014+ def mktemp(self, name='temp'):
1015+ """Customized mktemp that accepts an optional name argument."""
1016+ tempdir = os.path.join(self.tmpdir, name)
1017+ if os.path.exists(tempdir):
1018+ self.rmtree(tempdir)
1019+ self.makedirs(tempdir)
1020+ return tempdir
1021+
1022+ @property
1023+ def tmpdir(self):
1024+ """Default tmpdir: module/class/test_method."""
1025+ # check if we already generated the root path
1026+ root_dir = getattr(self, '__root', None)
1027+ if root_dir:
1028+ return root_dir
1029+ max_filename = 32 # some platforms limit lengths of filenames
1030+ base = os.path.join(self.__class__.__module__[:max_filename],
1031+ self.__class__.__name__[:max_filename],
1032+ self._testMethodName[:max_filename])
1033+ # use _trial_temp dir, it should be os.gwtcwd()
1034+ # define the root temp dir of the testcase, pylint: disable=W0201
1035+ self.__root = os.path.join(os.getcwd(), base)
1036+ return self.__root
1037+
1038+ def rmtree(self, path):
1039+ """Custom rmtree that handle ro parent(s) and childs."""
1040+ if not os.path.exists(path):
1041+ return
1042+ # change perms to rw, so we can delete the temp dir
1043+ if path != getattr(self, '__root', None):
1044+ os.chmod(os.path.dirname(path), 0755)
1045+ if not os.access(path, os.W_OK):
1046+ os.chmod(path, 0755)
1047+ # pylint: disable=W0612
1048+ for dirpath, dirs, files in os.walk(path):
1049+ for dirname in dirs:
1050+ if not os.access(os.path.join(dirpath, dirname), os.W_OK):
1051+ os.chmod(os.path.join(dirpath, dirname), 0777)
1052+ shutil.rmtree(path)
1053+
1054+ def makedirs(self, path):
1055+ """Custom makedirs that handle ro parent."""
1056+ parent = os.path.dirname(path)
1057+ if os.path.exists(parent):
1058+ os.chmod(parent, 0755)
1059+ os.makedirs(path)
1060
1061=== added file 'ubuntuone/devtools/testcases/dbus.py'
1062--- ubuntuone/devtools/testcases/dbus.py 1970-01-01 00:00:00 +0000
1063+++ ubuntuone/devtools/testcases/dbus.py 2012-01-04 20:35:34 +0000
1064@@ -0,0 +1,118 @@
1065+# -*- coding: utf-8 -*-
1066+#
1067+# Copyright 2009-2011 Canonical Ltd.
1068+#
1069+# This program is free software: you can redistribute it and/or modify it
1070+# under the terms of the GNU General Public License version 3, as published
1071+# by the Free Software Foundation.
1072+#
1073+# This program is distributed in the hope that it will be useful, but
1074+# WITHOUT ANY WARRANTY; without even the implied warranties of
1075+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1076+# PURPOSE. See the GNU General Public License for more details.
1077+#
1078+# You should have received a copy of the GNU General Public License along
1079+# with this program. If not, see <http://www.gnu.org/licenses/>.
1080+
1081+"""Base tests cases and test utilities."""
1082+
1083+from __future__ import absolute_import, with_statement
1084+
1085+import os
1086+
1087+from twisted.internet import defer
1088+
1089+# lint seems not tow work well when we use decorators
1090+# pylint:disable=W0611
1091+from ubuntuone.devtools.testcase import BaseTestCase, skipIf
1092+# pylint:enable=W0611
1093+# DBusRunner for DBusTestCase using tests
1094+from ubuntuone.devtools.services.dbus import DBusRunner
1095+
1096+
1097+# pylint: disable=F0401,C0103,W0406,E0611
1098+try:
1099+ import dbus
1100+except ImportError, e:
1101+ dbus = None
1102+
1103+try:
1104+ import dbus.service as service
1105+except ImportError:
1106+ service = None
1107+
1108+try:
1109+ from dbus.mainloop.glib import DBusGMainLoop
1110+except ImportError:
1111+ DBusGMainLoop = None
1112+
1113+# pylint: enable=F0401,C0103,W0406,E0611
1114+
1115+
1116+class InvalidSessionBus(Exception):
1117+ """Error when we are connected to the wrong session bus in tests."""
1118+
1119+
1120+class FakeDBusInterface(object):
1121+ """A fake DBusInterface..."""
1122+
1123+ def shutdown(self, with_restart=False):
1124+ """...that only knows how to go away"""
1125+
1126+
1127+@skipIf(dbus is None or service is None or DBusGMainLoop is None,
1128+ "The test requires dbus.")
1129+class DBusTestCase(BaseTestCase):
1130+ """Test the DBus event handling."""
1131+
1132+ def required_services(self):
1133+ """Return the list of required services for DBusTestCase."""
1134+ services = super(DBusTestCase, self).required_services()
1135+ services.extend([DBusRunner])
1136+ return services
1137+
1138+ @defer.inlineCallbacks
1139+ def setUp(self):
1140+ """Setup the infrastructure fo the test (dbus service)."""
1141+ # Class 'BaseTestCase' has no 'setUp' member
1142+ # pylint: disable=E1101
1143+ # dbus modules will be imported by the decorator
1144+ # pylint: disable=E0602
1145+ yield super(DBusTestCase, self).setUp()
1146+
1147+ # We need to ensure DBUS_SESSION_BUS_ADDRESS is private here
1148+ from urllib import unquote
1149+ bus_address = os.environ.get('DBUS_SESSION_BUS_ADDRESS', None)
1150+ if os.path.dirname(unquote(bus_address.split(',')[0].split('=')[1])) \
1151+ != os.path.dirname(os.getcwd()):
1152+ raise InvalidSessionBus('DBUS_SESSION_BUS_ADDRES is wrong.')
1153+
1154+ # Set up the main loop and bus connection
1155+ self.loop = DBusGMainLoop(set_as_default=True)
1156+ self.bus = dbus.bus.BusConnection(address_or_type=bus_address,
1157+ mainloop=self.loop)
1158+
1159+ # Monkeypatch the dbus.SessionBus/SystemBus methods, to ensure we
1160+ # always point at our own private bus instance.
1161+ self.patch(dbus, 'SessionBus', lambda: self.bus)
1162+ self.patch(dbus, 'SystemBus', lambda: self.bus)
1163+
1164+ # Check that we are on the correct bus for real
1165+# Disable this for now, because our tests are extremely broken :(
1166+# bus_names = self.bus.list_names()
1167+# if len(bus_names) > 2:
1168+# raise InvalidSessionBus('Too many bus connections: %s (%r)' %
1169+# (len(bus_names), bus_names))
1170+
1171+ # monkeypatch busName.__del__ to avoid errors on gc
1172+ # we take care of releasing the name in shutdown
1173+ service.BusName.__del__ = lambda _: None
1174+ yield self.bus.set_exit_on_disconnect(False)
1175+ self.signal_receivers = set()
1176+
1177+ @defer.inlineCallbacks
1178+ def tearDown(self):
1179+ """Cleanup the test."""
1180+ yield self.bus.flush()
1181+ yield self.bus.close()
1182+ yield super(DBusTestCase, self).tearDown()
1183
1184=== added directory 'ubuntuone/devtools/testing'
1185=== added file 'ubuntuone/devtools/testing/__init__.py'
1186--- ubuntuone/devtools/testing/__init__.py 1970-01-01 00:00:00 +0000
1187+++ ubuntuone/devtools/testing/__init__.py 2012-01-04 20:35:34 +0000
1188@@ -0,0 +1,1 @@
1189+"""Testing helpers."""
1190
1191=== added file 'ubuntuone/devtools/testing/txcheck.py'
1192--- ubuntuone/devtools/testing/txcheck.py 1970-01-01 00:00:00 +0000
1193+++ ubuntuone/devtools/testing/txcheck.py 2012-01-04 20:35:34 +0000
1194@@ -0,0 +1,366 @@
1195+# -*- coding: utf-8 -*-
1196+
1197+# Author: Tim Cole <tim.cole@canonical.com>
1198+#
1199+# Copyright 2011 Canonical Ltd.
1200+#
1201+# This program is free software: you can redistribute it and/or modify it
1202+# under the terms of the GNU General Public License version 3, as published
1203+# by the Free Software Foundation.
1204+#
1205+# This program is distributed in the hope that it will be useful, but
1206+# WITHOUT ANY WARRANTY; without even the implied warranties of
1207+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1208+# PURPOSE. See the GNU General Public License for more details.
1209+#
1210+# You should have received a copy of the GNU General Public License along
1211+# with this program. If not, see <http://www.gnu.org/licenses/>.
1212+
1213+"""Utilities for performing correctness checks."""
1214+
1215+import sys
1216+import ast
1217+from inspect import getsource
1218+from textwrap import dedent
1219+from itertools import takewhile
1220+from unittest import TestCase, TestSuite, TestResult
1221+
1222+from twisted.trial.unittest import TestCase as TwistedTestCase
1223+
1224+
1225+def type_to_name(type_obj):
1226+ """Return a name for a type."""
1227+ package_name = getattr(type_obj, '__module__', None)
1228+ if package_name:
1229+ return "%s.%s" % (package_name, type_obj.__name__)
1230+ else:
1231+ return type_obj.__name__
1232+
1233+
1234+class Problem(AssertionError):
1235+ """An object representing a problem in a method."""
1236+
1237+ def __init__(self, method, test_class, ancestor_class):
1238+ """Initialize an instance."""
1239+ super(Problem, self).__init__()
1240+ self.method = method
1241+ self.test_class = test_class
1242+ self.ancestor_class = ancestor_class
1243+
1244+ def __eq__(self, other):
1245+ """Test equality."""
1246+ return type(self) == type(other) and self.__dict__ == other.__dict__
1247+
1248+ def __ne__(self, other):
1249+ """Test inequality."""
1250+ return not (self == other)
1251+
1252+ def __hash__(self):
1253+ """Return hash."""
1254+ member_hash = 0
1255+ for (key, value) in self.__dict__.iteritems():
1256+ member_hash ^= hash(key) ^ hash(value)
1257+ return hash(type(self)) ^ member_hash
1258+
1259+ def __str__(self):
1260+ """Return a friendlier representation."""
1261+ if self.ancestor_class != self.test_class:
1262+ method_string = ("%s in ancestor method %s.%s" %
1263+ (type_to_name(self.test_class),
1264+ type_to_name(self.ancestor_class),
1265+ self.method))
1266+ else:
1267+ method_string = ("%s.%s" %
1268+ (type_to_name(self.test_class), self.method))
1269+ return ("%s for %s" % (type(self).__name__, method_string))
1270+
1271+ def __repr__(self):
1272+ """Return representation string."""
1273+ return "<%s %r>" % (type(self), self.__dict__)
1274+
1275+
1276+class MethodShadowed(Problem):
1277+ """Problem when trial's run method is shadowed."""
1278+
1279+
1280+class SuperResultDiscarded(Problem):
1281+ """Problem when callback chains are broken."""
1282+
1283+
1284+class SuperNotCalled(Problem):
1285+ """Problem when super isn't called."""
1286+
1287+
1288+class MissingInlineCallbacks(Problem):
1289+ """Problem when the inlineCallbacks decorator is missing."""
1290+
1291+
1292+class MissingReturnValue(Problem):
1293+ """Problem when there's no return value."""
1294+
1295+
1296+def match_type(expected_type):
1297+ """Return predicate matching nodes of given type."""
1298+ return lambda node: isinstance(node, expected_type)
1299+
1300+
1301+def match_equal(expected_value):
1302+ """Return predicate matching nodes equaling the given value."""
1303+ return lambda node: expected_value == node
1304+
1305+
1306+def match_in(expected_values):
1307+ """Return predicate matching node if in collection of expected values."""
1308+ return lambda node: node in expected_values
1309+
1310+
1311+def match_not_none():
1312+ """Returns a predicate matching nodes which are not None."""
1313+ return lambda node: node is not None
1314+
1315+
1316+def match_any(*subtests):
1317+ """Return short-circuiting predicate matching any given subpredicate."""
1318+ if len(subtests) == 1:
1319+ return subtests[0]
1320+ else:
1321+
1322+ def test(node):
1323+ """Try each subtest until we find one that passes."""
1324+ for subtest in subtests:
1325+ if subtest(node):
1326+ return True
1327+ return False
1328+
1329+ return test
1330+
1331+
1332+def match_all(*subtests):
1333+ """Return short-circuiting predicate matching all given subpredicates."""
1334+ if len(subtests) == 1:
1335+ return subtests[0]
1336+ else:
1337+
1338+ def test(node):
1339+ """Try each subtest until we find one that fails."""
1340+ for subtest in subtests:
1341+ if not subtest(node):
1342+ return False
1343+ return True
1344+
1345+ return test
1346+
1347+
1348+def match_attr(attr_name, *tests):
1349+ """Return predicate matching subpredicates against an attribute value."""
1350+ return lambda node: match_all(*tests)(getattr(node, attr_name))
1351+
1352+
1353+def match_path(initial_test, *components):
1354+ """Return predicate which recurses into the tree via given attributes."""
1355+ components = list(components)
1356+ components.reverse()
1357+ test = lambda node: True
1358+ for component in components:
1359+ attr_name = component[0]
1360+ subtests = component[1:]
1361+ test = match_attr(attr_name, match_all(match_all(*subtests), test))
1362+ return match_all(initial_test, test)
1363+
1364+
1365+def match_child(*tests):
1366+ """Return predicate which tests any child."""
1367+ subtest = match_all(*tests)
1368+
1369+ def test(node):
1370+ """Try each child until we find one that matches."""
1371+ for child in ast.iter_child_nodes(node):
1372+ if subtest(child):
1373+ return True
1374+ return False
1375+
1376+ return test
1377+
1378+
1379+def match_descendant(subtest, prune):
1380+ """Return predicate which tests a node and any descendants."""
1381+
1382+ def test(node):
1383+ """Recursively (breadth-first) search for a matching node."""
1384+ for child in ast.iter_child_nodes(node):
1385+ if prune(child):
1386+ continue
1387+ if subtest(child) or test(child):
1388+ return True
1389+ return False
1390+
1391+ return test
1392+
1393+
1394+def matches(node, *tests):
1395+ """Convenience function to try predicates on a node."""
1396+ return match_all(*tests)(node)
1397+
1398+
1399+def any_matches(nodes, *tests):
1400+ """Convenience function to try predicates on any of a sequence of nodes."""
1401+ test = match_all(*tests)
1402+ for node in nodes:
1403+ if test(node):
1404+ return True
1405+ return False
1406+
1407+
1408+def iter_matching_child_nodes(node, *tests):
1409+ """Yields every matching child node."""
1410+ test = match_all(*tests)
1411+ for child in ast.iter_child_nodes(node):
1412+ if test(child):
1413+ yield child
1414+
1415+
1416+SETUP_FUNCTION_NAMES = ('setUp', 'tearDown')
1417+SETUP_FUNCTION = match_path(match_type(ast.FunctionDef),
1418+ ('name', match_in(SETUP_FUNCTION_NAMES)))
1419+
1420+SUPER = match_path(match_type(ast.Call),
1421+ ('func', match_type(ast.Attribute)),
1422+ ('value', match_type(ast.Call)),
1423+ ('func', match_type(ast.Name)),
1424+ ('id', match_equal("super")))
1425+
1426+BARE_SUPER = match_path(match_type(ast.Expr),
1427+ ('value', SUPER))
1428+
1429+YIELD = match_type(ast.Yield)
1430+
1431+INLINE_CALLBACKS_DECORATOR = \
1432+ match_any(match_path(match_type(ast.Attribute),
1433+ ('attr', match_equal('inlineCallbacks'))),
1434+ match_path(match_type(ast.Name),
1435+ ('id', match_equal('inlineCallbacks'))))
1436+
1437+RETURN_VALUE = \
1438+ match_path(match_type(ast.Return),
1439+ ('value', match_not_none()))
1440+
1441+DEFS = match_any(match_type(ast.ClassDef),
1442+ match_type(ast.FunctionDef))
1443+
1444+
1445+def find_problems(class_to_check):
1446+ """Check twisted test setup in a given test class."""
1447+ mro = class_to_check.__mro__
1448+ if TwistedTestCase not in mro:
1449+ return set()
1450+
1451+ problems = set()
1452+
1453+ ancestry = takewhile(lambda c: c != TwistedTestCase, mro)
1454+ for ancestor_class in ancestry:
1455+ if 'run' in ancestor_class.__dict__:
1456+ problem = MethodShadowed(method='run',
1457+ test_class=class_to_check,
1458+ ancestor_class=ancestor_class)
1459+ problems.add(problem)
1460+
1461+ source = dedent(getsource(ancestor_class))
1462+ tree = ast.parse(source)
1463+ # the top level of the tree is a Module
1464+ class_node = tree.body[0]
1465+
1466+ # Check setUp/tearDown
1467+ for def_node in iter_matching_child_nodes(class_node, SETUP_FUNCTION):
1468+ if matches(def_node, match_child(BARE_SUPER)):
1469+ # Superclass method called, but its result wasn't used
1470+ problem = SuperResultDiscarded(method=def_node.name,
1471+ test_class=class_to_check,
1472+ ancestor_class=ancestor_class)
1473+ problems.add(problem)
1474+ if not matches(def_node, match_descendant(SUPER, DEFS)):
1475+ # The call to the overridden superclass method is missing
1476+ problem = SuperNotCalled(method=def_node.name,
1477+ test_class=class_to_check,
1478+ ancestor_class=ancestor_class)
1479+ problems.add(problem)
1480+
1481+ decorators = def_node.decorator_list
1482+
1483+ if matches(def_node, match_descendant(YIELD, DEFS)):
1484+ # Yield was used, making this a generator
1485+ if not any_matches(decorators, INLINE_CALLBACKS_DECORATOR):
1486+ # ...but the inlineCallbacks decorator is missing
1487+ problem = MissingInlineCallbacks(
1488+ method=def_node.name,
1489+ test_class=class_to_check,
1490+ ancestor_class=ancestor_class)
1491+ problems.add(problem)
1492+ else:
1493+ if not matches(def_node, match_descendant(RETURN_VALUE, DEFS)):
1494+ # The function fails to return a deferred
1495+ problem = MissingReturnValue(
1496+ method=def_node.name,
1497+ test_class=class_to_check,
1498+ ancestor_class=ancestor_class)
1499+ problems.add(problem)
1500+
1501+ return problems
1502+
1503+
1504+def get_test_classes(suite):
1505+ """Return all the unique test classes involved in a suite."""
1506+ classes = set()
1507+
1508+ def find_classes(suite_or_test):
1509+ """Recursively find all the test classes."""
1510+ if isinstance(suite_or_test, TestSuite):
1511+ for subtest in suite_or_test:
1512+ find_classes(subtest)
1513+ else:
1514+ classes.add(type(suite_or_test))
1515+
1516+ find_classes(suite)
1517+
1518+ return classes
1519+
1520+
1521+def make_check_testcase(tests):
1522+ """Make TestCase which checks the given twisted tests."""
1523+
1524+ class TXCheckTest(TestCase):
1525+ """Test case which checks the test classes for problems."""
1526+
1527+ def runTest(self): # pylint: disable=C0103
1528+ """Do nothing."""
1529+
1530+ def run(self, result=None):
1531+ """Check all the test classes for problems."""
1532+ if result is None:
1533+ result = TestResult()
1534+
1535+ test_classes = set()
1536+
1537+ for test_object in tests:
1538+ test_classes |= get_test_classes(test_object)
1539+
1540+ for test_class in test_classes:
1541+ problems = find_problems(test_class)
1542+ for problem in problems:
1543+ try:
1544+ raise problem
1545+ except Problem:
1546+ result.addFailure(self, sys.exc_info())
1547+
1548+ return TXCheckTest()
1549+
1550+
1551+class TXCheckSuite(TestSuite):
1552+ """Test suite which checks twisted tests."""
1553+
1554+ def __init__(self, tests=()):
1555+ """Initialize with the given tests, and add a special test."""
1556+
1557+ tests = list(tests)
1558+ tests.insert(0, make_check_testcase(self))
1559+
1560+ super(TXCheckSuite, self).__init__(tests)

Subscribers

People subscribed via source and target branches

to all changes: