summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert Collins <robertc@robertcollins.net>2014-11-14 00:00:38 +1300
committerRobert Collins <robertc@robertcollins.net>2014-11-14 08:40:39 +1300
commitdfe62afa96970d059543a50c70047fa05b8c0bea (patch)
tree3dd29fd5fcc9b310ab939c8170ecc3f01144c422
parent377ea8f7eedc0d539684e28c54b024c85f3f9c40 (diff)
downloadtesttools-dfe62afa96970d059543a50c70047fa05b8c0bea.tar.gz
Use unittest2 TestProgram implementation.
This brings in many fixes made to discovery where previously we were only using the discovery package or the version in the release of Python that the test execution was occuring on. (Robert Collins, #1271133) Change-Id: I6270c8e8949262c01cb3a4e40735077ad6fc1ef2
-rw-r--r--NEWS5
-rwxr-xr-xtesttools/run.py225
-rw-r--r--testtools/tests/test_run.py10
3 files changed, 42 insertions, 198 deletions
diff --git a/NEWS b/NEWS
index adf4158..8c62c27 100644
--- a/NEWS
+++ b/NEWS
@@ -10,6 +10,11 @@ NEXT
Changes
-------
+* Depends on unittest2 for discovery functionality and the ``TestProgram`` base
+ class. This brings in many fixes made to discovery where previously we were
+ only using the discovery package or the version in the release of Python
+ that the test execution was occuring on. (Robert Collins, #1271133)
+
* Fixed unit tests which were failing under pypy due to a change in the way
pypy formats tracebacks. (Thomi Richards)
diff --git a/testtools/run.py b/testtools/run.py
index c8170a7..8f4b2a5 100755
--- a/testtools/run.py
+++ b/testtools/run.py
@@ -8,6 +8,7 @@ For instance, to run the testtools test suite.
$ python -m testtools.run testtools.tests.test_suite
"""
+import argparse
from functools import partial
import os.path
import unittest2 as unittest
@@ -25,6 +26,11 @@ defaultTestLoaderCls = unittest.TestLoader
have_discover = True
discover_impl = unittest.loader
+# Kept for API compatibility, but no longer used.
+BUFFEROUTPUT = ""
+CATCHBREAK = ""
+FAILFAST = ""
+USAGE_AS_MAIN = ""
def list_test(test):
"""Return the test ids that would be run if test() was run.
@@ -100,75 +106,23 @@ class TestToolsTestRunner(object):
# Taken from python 2.7 and slightly modified for compatibility with
# older versions. Delete when 2.7 is the oldest supported version.
# Modifications:
-# - Use have_discover to raise an error if the user tries to use
-# discovery on an old version and doesn't have discover installed.
-# - If --catch is given check that installHandler is available, as
-# it won't be on old python versions.
-# - print calls have been been made single-source python3 compatibile.
-# - exception handling likewise.
-# - The default help has been changed to USAGE_AS_MAIN and USAGE_FROM_MODULE
-# removed.
-# - A tweak has been added to detect 'python -m *.run' and use a
-# better progName in that case.
-# - self.module is more comprehensively set to None when being invoked from
-# the commandline - __name__ is used as a sentinel value.
+# - If --catch is given, check that installHandler is available, as
+# it won't be on old python versions or python builds without signals.
# - --list has been added which can list tests (should be upstreamed).
# - --load-list has been added which can reduce the tests used (should be
# upstreamed).
-# - The limitation of using getopt is declared to the user.
-# - http://bugs.python.org/issue16709 is worked around, by sorting tests when
-# discover is used.
-# - We monkey-patch the discover and unittest loaders to address
-# http://bugs.python.org/issue16662 with the proposed upstream patch.
-
-FAILFAST = " -f, --failfast Stop on first failure\n"
-CATCHBREAK = " -c, --catch Catch control-C and display results\n"
-BUFFEROUTPUT = " -b, --buffer Buffer stdout and stderr during test runs\n"
-
-USAGE_AS_MAIN = """\
-Usage: %(progName)s [options] [tests]
-
-Options:
- -h, --help Show this message
- -v, --verbose Verbose output
- -q, --quiet Minimal output
- -l, --list List tests rather than executing them.
- --load-list Specifies a file containing test ids, only tests matching
- those ids are executed.
-%(failfast)s%(catchbreak)s%(buffer)s
-Examples:
- %(progName)s test_module - run tests from test_module
- %(progName)s module.TestClass - run tests from module.TestClass
- %(progName)s module.Class.test_method - run specified test method
-
-All options must come before [tests]. [tests] can be a list of any number of
-test modules, classes and test methods.
-
-Alternative Usage: %(progName)s discover [options]
-
-Options:
- -v, --verbose Verbose output
-%(failfast)s%(catchbreak)s%(buffer)s -s directory Directory to start discovery ('.' default)
- -p pattern Pattern to match test files ('test*.py' default)
- -t directory Top level directory of project (default to
- start directory)
- -l, --list List tests rather than executing them.
- --load-list Specifies a file containing test ids, only tests matching
- those ids are executed.
-
-For test discovery all test modules must be importable from the top
-level directory of the project.
-"""
-class TestProgram(object):
+class TestProgram(unittest.TestProgram):
"""A command-line program that runs a set of tests; this is primarily
for making test modules conveniently executable.
"""
- USAGE = USAGE_AS_MAIN
# defaults for testing
+ module=None
+ verbosity = 1
failfast = catchbreak = buffer = progName = None
+ _discovery_parser = None
def __init__(self, module=__name__, defaultTest=None, argv=None,
testRunner=None, testLoader=defaultTestLoader,
@@ -194,6 +148,7 @@ class TestProgram(object):
self.verbosity = verbosity
self.buffer = buffer
self.defaultTest = defaultTest
+ # XXX: Local edit (see http://bugs.python.org/issue22860)
self.listtests = False
self.load_list = None
self.testRunner = testRunner
@@ -206,6 +161,7 @@ class TestProgram(object):
progName = os.path.basename(argv[0])
self.progName = progName
self.parseArgs(argv)
+ # XXX: Local edit (see http://bugs.python.org/issue22860)
if self.load_list:
# TODO: preserve existing suites (like testresources does in
# OptimisingTestSuite.add, but with a standard protocol).
@@ -218,6 +174,7 @@ class TestProgram(object):
source.close()
test_ids = set(line.strip().decode('utf-8') for line in lines)
self.test = filter_by_ids(self.test, test_ids)
+ # XXX: Local edit (see http://bugs.python.org/issue22860)
if not self.listtests:
self.runTests()
else:
@@ -228,144 +185,23 @@ class TestProgram(object):
for test in iterate_tests(self.test):
self.stdout.write('%s\n' % test.id())
- def usageExit(self, msg=None):
- if msg:
- print(msg)
- usage = {'progName': self.progName, 'catchbreak': '', 'failfast': '',
- 'buffer': ''}
- if self.failfast != False:
- usage['failfast'] = FAILFAST
- if self.catchbreak != False:
- usage['catchbreak'] = CATCHBREAK
- if self.buffer != False:
- usage['buffer'] = BUFFEROUTPUT
- print(self.USAGE % usage)
- sys.exit(2)
-
- def parseArgs(self, argv):
- if len(argv) > 1 and argv[1].lower() == 'discover':
- self._do_discovery(argv[2:])
- return
-
- import getopt
- long_opts = ['help', 'verbose', 'quiet', 'failfast', 'catch', 'buffer',
- 'list', 'load-list=']
- try:
- options, args = getopt.getopt(argv[1:], 'hHvqfcbl', long_opts)
- for opt, value in options:
- if opt in ('-h','-H','--help'):
- self.usageExit()
- if opt in ('-q','--quiet'):
- self.verbosity = 0
- if opt in ('-v','--verbose'):
- self.verbosity = 2
- if opt in ('-f','--failfast'):
- if self.failfast is None:
- self.failfast = True
- # Should this raise an exception if -f is not valid?
- if opt in ('-c','--catch'):
- if self.catchbreak is None:
- self.catchbreak = True
- # Should this raise an exception if -c is not valid?
- if opt in ('-b','--buffer'):
- if self.buffer is None:
- self.buffer = True
- # Should this raise an exception if -b is not valid?
- if opt in ('-l', '--list'):
- self.listtests = True
- if opt == '--load-list':
- self.load_list = value
- if len(args) == 0 and self.defaultTest is None:
- # createTests will load tests from self.module
- self.testNames = None
- elif len(args) > 0:
- self.testNames = args
- else:
- self.testNames = (self.defaultTest,)
- self.createTests()
- except getopt.error:
- self.usageExit(sys.exc_info()[1])
-
- def createTests(self):
- if self.testNames is None:
- self.test = self.testLoader.loadTestsFromModule(self.module)
- else:
- self.test = self.testLoader.loadTestsFromNames(self.testNames,
- self.module)
-
- def _do_discovery(self, argv, Loader=defaultTestLoaderCls):
- # handle command line args for test discovery
- if not have_discover:
- raise AssertionError("Unable to use discovery, must use python 2.7 "
- "or greater, or install the discover package.")
- self.progName = '%s discover' % self.progName
- import optparse
- parser = optparse.OptionParser()
- parser.prog = self.progName
- parser.add_option('-v', '--verbose', dest='verbose', default=False,
- help='Verbose output', action='store_true')
- if self.failfast != False:
- parser.add_option('-f', '--failfast', dest='failfast', default=False,
- help='Stop on first fail or error',
- action='store_true')
- if self.catchbreak != False:
- parser.add_option('-c', '--catch', dest='catchbreak', default=False,
- help='Catch ctrl-C and display results so far',
- action='store_true')
- if self.buffer != False:
- parser.add_option('-b', '--buffer', dest='buffer', default=False,
- help='Buffer stdout and stderr during tests',
- action='store_true')
- parser.add_option('-s', '--start-directory', dest='start', default='.',
- help="Directory to start discovery ('.' default)")
- parser.add_option('-p', '--pattern', dest='pattern', default='test*.py',
- help="Pattern to match tests ('test*.py' default)")
- parser.add_option('-t', '--top-level-directory', dest='top', default=None,
- help='Top level directory of project (defaults to start directory)')
- parser.add_option('-l', '--list', dest='listtests', default=False, action="store_true",
- help='List tests rather than running them.')
- parser.add_option('--load-list', dest='load_list', default=None,
- help='Specify a filename containing the test ids to use.')
-
- options, args = parser.parse_args(argv)
- if len(args) > 3:
- self.usageExit()
-
- for name, value in zip(('start', 'pattern', 'top'), args):
- setattr(options, name, value)
-
- # only set options from the parsing here
- # if they weren't set explicitly in the constructor
- if self.failfast is None:
- self.failfast = options.failfast
- if self.catchbreak is None:
- self.catchbreak = options.catchbreak
- if self.buffer is None:
- self.buffer = options.buffer
- self.listtests = options.listtests
- self.load_list = options.load_list
-
- if options.verbose:
- self.verbosity = 2
-
- start_dir = options.start
- pattern = options.pattern
- top_level_dir = options.top
-
- loader = Loader()
- # See http://bugs.python.org/issue16709
- # While sorting here is intrusive, its better than being random.
- # Rules for the sort:
- # - standard suites are flattened, and the resulting tests sorted by
- # id.
- # - non-standard suites are preserved as-is, and sorted into position
- # by the first test found by iterating the suite.
- # We do this by a DSU process: flatten and grab a key, sort, strip the
- # keys.
- loaded = loader.discover(start_dir, pattern, top_level_dir)
- self.test = sorted_tests(loaded)
+ def _getParentArgParser(self):
+ parser = super(TestProgram, self)._getParentArgParser()
+ # XXX: Local edit (see http://bugs.python.org/issue22860)
+ parser.add_argument('-l', '--list', dest='listtests', default=False,
+ action='store_true', help='List tests rather than executing them')
+ parser.add_argument('--load-list', dest='load_list', default=None,
+ help='Specifies a file containing test ids, only tests matching '
+ 'those ids are executed')
+ return parser
+
+ def _do_discovery(self, argv, Loader=None):
+ super(TestProgram, self)._do_discovery(argv, Loader=Loader)
+ # XXX: Local edit (see http://bugs.python.org/issue22860)
+ self.test = sorted_tests(self.test)
def runTests(self):
+ # XXX: Local edit (see http://bugs.python.org/issue22860)
if (self.catchbreak
and getattr(unittest, 'installHandler', None) is not None):
unittest.installHandler()
@@ -375,6 +211,7 @@ class TestProgram(object):
sys.exit(not self.result.wasSuccessful())
def _get_runner(self):
+ # XXX: Local edit (see http://bugs.python.org/issue22860)
if self.testRunner is None:
self.testRunner = TestToolsTestRunner
try:
diff --git a/testtools/tests/test_run.py b/testtools/tests/test_run.py
index ac4b9dd..fd564c7 100644
--- a/testtools/tests/test_run.py
+++ b/testtools/tests/test_run.py
@@ -9,6 +9,7 @@ from textwrap import dedent
from extras import try_import
fixtures = try_import('fixtures')
testresources = try_import('testresources')
+import unittest2
import testtools
from testtools import TestCase, run, skipUnless
@@ -105,7 +106,7 @@ def test_suite():
testtools.__path__.append(self.package.base)
-if fixtures and run.have_discover:
+if fixtures:
class SampleLoadTestsPackage(fixtures.Fixture):
"""Creates a test suite package using load_tests."""
@@ -168,10 +169,10 @@ testtools.runexample.TestFoo.test_quux
""", out.getvalue())
def test_run_list_failed_import(self):
- if not run.have_discover:
- self.skipTest("Need discover")
broken = self.useFixture(SampleTestFixture(broken=True))
out = StringIO()
+ # XXX: http://bugs.python.org/issue22811
+ unittest2.defaultTestLoader._top_level_dir = None
exc = self.assertRaises(
SystemExit,
run.main, ['prog', 'discover', '-l', broken.package.base, '*.py'], out)
@@ -287,7 +288,6 @@ Ran 2 tests in \\d.\\d\\d\\ds
OK
""")))
- @skipUnless(run.have_discover, "discovery not present")
@skipUnless(fixtures, "fixtures not present")
def test_issue_16662(self):
# unittest's discover implementation didn't handle load_tests on
@@ -296,6 +296,8 @@ OK
# See http://bugs.python.org/issue16662
pkg = self.useFixture(SampleLoadTestsPackage())
out = StringIO()
+ # XXX: http://bugs.python.org/issue22811
+ unittest2.defaultTestLoader._top_level_dir = None
self.assertEqual(None, run.main(
['prog', 'discover', '-l', pkg.package.base], out))
self.assertEqual(dedent("""\