Merged revisions 72570,72582-72583,73027,73049,73071,73151,73247 via svnmerge from

svn+ssh://pythondev@svn.python.org/python/trunk

........
  r72570 | michael.foord | 2009-05-11 12:59:43 -0500 (Mon, 11 May 2009) | 7 lines

  Adds a verbosity keyword argument to unittest.main plus a minor fix allowing you to specify test modules / classes
  from the command line.

  Closes issue 5995.

  Michael Foord
........
  r72582 | michael.foord | 2009-05-12 05:46:23 -0500 (Tue, 12 May 2009) | 1 line

  Fix to restore command line behaviour for test modules using unittest.main(). Regression caused by issue 5995. Michael
........
  r72583 | michael.foord | 2009-05-12 05:49:13 -0500 (Tue, 12 May 2009) | 1 line

  Better fix for modules using unittest.main(). Fixes regression caused by commit for issue 5995. Michael Foord
........
  r73027 | michael.foord | 2009-05-29 15:33:46 -0500 (Fri, 29 May 2009) | 1 line

  Add test discovery to unittest. Issue 6001.
........
  r73049 | georg.brandl | 2009-05-30 05:45:40 -0500 (Sat, 30 May 2009) | 1 line

  Rewrap a few long lines.
........
  r73071 | georg.brandl | 2009-05-31 09:15:25 -0500 (Sun, 31 May 2009) | 1 line

  Fix markup.
........
  r73151 | michael.foord | 2009-06-02 13:08:27 -0500 (Tue, 02 Jun 2009) | 1 line

  Restore default testRunner argument in unittest.main to None. Issue 6177
........
  r73247 | michael.foord | 2009-06-05 09:14:34 -0500 (Fri, 05 Jun 2009) | 1 line

  Fix unittest discovery tests for Windows. Issue 6199
........
This commit is contained in:
Benjamin Peterson 2009-06-27 23:45:02 +00:00
parent f7a6b508ce
commit d2397753ee
3 changed files with 709 additions and 55 deletions

View file

@ -56,6 +56,9 @@ import traceback
import types
import warnings
from fnmatch import fnmatch
##############################################################################
# Exported classes and functions
##############################################################################
@ -1228,6 +1231,7 @@ class TestLoader(object):
testMethodPrefix = 'test'
sortTestMethodsUsing = staticmethod(three_way_cmp)
suiteClass = TestSuite
_top_level_dir = None
def loadTestsFromTestCase(self, testCaseClass):
"""Return a suite of all tests cases contained in testCaseClass"""
@ -1240,13 +1244,17 @@ class TestLoader(object):
suite = self.suiteClass(map(testCaseClass, testCaseNames))
return suite
def loadTestsFromModule(self, module):
def loadTestsFromModule(self, module, use_load_tests=True):
"""Return a suite of all tests cases contained in the given module"""
tests = []
for name in dir(module):
obj = getattr(module, name)
if isinstance(obj, type) and issubclass(obj, TestCase):
tests.append(self.loadTestsFromTestCase(obj))
load_tests = getattr(module, 'load_tests', None)
if use_load_tests and load_tests is not None:
return load_tests(self, tests, None)
return self.suiteClass(tests)
def loadTestsFromName(self, name, module=None):
@ -1320,7 +1328,97 @@ class TestLoader(object):
testFnNames.sort(key=CmpToKey(self.sortTestMethodsUsing))
return testFnNames
def discover(self, start_dir, pattern='test*.py', top_level_dir=None):
"""Find and return all test modules from the specified start
directory, recursing into subdirectories to find them. Only test files
that match the pattern will be loaded. (Using shell style pattern
matching.)
All test modules must be importable from the top level of the project.
If the start directory is not the top level directory then the top
level directory must be specified separately.
If a test package name (directory with '__init__.py') matches the
pattern then the package will be checked for a 'load_tests' function. If
this exists then it will be called with loader, tests, pattern.
If load_tests exists then discovery does *not* recurse into the package,
load_tests is responsible for loading all tests in the package.
The pattern is deliberately not stored as a loader attribute so that
packages can continue discovery themselves. top_level_dir is stored so
load_tests does not need to pass this argument in to loader.discover().
"""
if top_level_dir is None and self._top_level_dir is not None:
# make top_level_dir optional if called from load_tests in a package
top_level_dir = self._top_level_dir
elif top_level_dir is None:
top_level_dir = start_dir
top_level_dir = os.path.abspath(os.path.normpath(top_level_dir))
start_dir = os.path.abspath(os.path.normpath(start_dir))
if not top_level_dir in sys.path:
# all test modules must be importable from the top level directory
sys.path.append(top_level_dir)
self._top_level_dir = top_level_dir
if start_dir != top_level_dir and not os.path.isfile(os.path.join(start_dir, '__init__.py')):
# what about __init__.pyc or pyo (etc)
raise ImportError('Start directory is not importable: %r' % start_dir)
tests = list(self._find_tests(start_dir, pattern))
return self.suiteClass(tests)
def _get_module_from_path(self, path):
"""Load a module from a path relative to the top-level directory
of a project. Used by discovery."""
path = os.path.splitext(os.path.normpath(path))[0]
relpath = os.path.relpath(path, self._top_level_dir)
assert not os.path.isabs(relpath), "Path must be within the project"
assert not relpath.startswith('..'), "Path must be within the project"
name = relpath.replace(os.path.sep, '.')
__import__(name)
return sys.modules[name]
def _find_tests(self, start_dir, pattern):
"""Used by discovery. Yields test suites it loads."""
paths = os.listdir(start_dir)
for path in paths:
full_path = os.path.join(start_dir, path)
# what about __init__.pyc or pyo (etc)
# we would need to avoid loading the same tests multiple times
# from '.py', '.pyc' *and* '.pyo'
if os.path.isfile(full_path) and path.lower().endswith('.py'):
if fnmatch(path, pattern):
# if the test file matches, load it
module = self._get_module_from_path(full_path)
yield self.loadTestsFromModule(module)
elif os.path.isdir(full_path):
if not os.path.isfile(os.path.join(full_path, '__init__.py')):
continue
load_tests = None
tests = None
if fnmatch(path, pattern):
# only check load_tests if the package directory itself matches the filter
package = self._get_module_from_path(full_path)
load_tests = getattr(package, 'load_tests', None)
tests = self.loadTestsFromModule(package, use_load_tests=False)
if load_tests is None:
if tests is not None:
# tests loaded from package file
yield tests
# recurse into the package
for test in self._find_tests(full_path, pattern):
yield test
else:
yield load_tests(self, tests, pattern)
defaultTestLoader = TestLoader()
@ -1525,11 +1623,37 @@ class TextTestRunner(object):
# Facilities for running tests from the command line
##############################################################################
class TestProgram(object):
"""A command-line program that runs a set of tests; this is primarily
for making test modules conveniently executable.
"""
USAGE = """\
USAGE_AS_MAIN = """\
Usage: %(progName)s [options] [tests]
Options:
-h, --help Show this message
-v, --verbose Verbose output
-q, --quiet Minimal output
Examples:
%(progName)s test_module - run tests from test_module
%(progName)s test_module.TestClass - run tests from
test_module.TestClass
%(progName)s test_module.TestClass.test_method - run specified test method
[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
-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)
For test discovery all test modules must be importable from the top
level directory of the project.
"""
USAGE_FROM_MODULE = """\
Usage: %(progName)s [options] [test] [...]
Options:
@ -1544,9 +1668,24 @@ Examples:
%(progName)s MyTestCase - run all 'test*' test methods
in MyTestCase
"""
if __name__ == '__main__':
USAGE = USAGE_AS_MAIN
else:
USAGE = USAGE_FROM_MODULE
class TestProgram(object):
"""A command-line program that runs a set of tests; this is primarily
for making test modules conveniently executable.
"""
USAGE = USAGE
def __init__(self, module='__main__', defaultTest=None,
argv=None, testRunner=TextTestRunner,
testLoader=defaultTestLoader, exit=True):
argv=None, testRunner=None,
testLoader=defaultTestLoader, exit=True,
verbosity=1):
if testRunner is None:
testRunner = TextTestRunner
if isinstance(module, str):
self.module = __import__(module)
for part in module.split('.')[1:]:
@ -1557,7 +1696,7 @@ Examples:
argv = sys.argv
self.exit = exit
self.verbosity = 1
self.verbosity = verbosity
self.defaultTest = defaultTest
self.testRunner = testRunner
self.testLoader = testLoader
@ -1572,6 +1711,10 @@ Examples:
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']
try:
@ -1588,6 +1731,9 @@ Examples:
return
if len(args) > 0:
self.testNames = args
if __name__ == '__main__':
# to support python -m unittest ...
self.module = None
else:
self.testNames = (self.defaultTest,)
self.createTests()
@ -1598,6 +1744,36 @@ Examples:
self.test = self.testLoader.loadTestsFromNames(self.testNames,
self.module)
def _do_discovery(self, argv, Loader=TestLoader):
# handle command line args for test discovery
import optparse
parser = optparse.OptionParser()
parser.add_option('-v', '--verbose', dest='verbose', default=False,
help='Verbose output', 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)')
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)
if options.verbose:
self.verbosity = 2
start_dir = options.start
pattern = options.pattern
top_level_dir = options.top
loader = Loader()
self.test = loader.discover(start_dir, pattern, top_level_dir)
def runTests(self):
if isinstance(self.testRunner, type):
try:
@ -1620,4 +1796,5 @@ main = TestProgram
##############################################################################
if __name__ == "__main__":
sys.modules['unittest'] = sys.modules['__main__']
main(module=None)