diff options
author | Benjamin Peterson <benjamin@python.org> | 2009-06-27 23:45:02 (GMT) |
---|---|---|
committer | Benjamin Peterson <benjamin@python.org> | 2009-06-27 23:45:02 (GMT) |
commit | d2397753ee3d20579aa60b5e1c037e9e20db7ccb (patch) | |
tree | 8ad290dc45baa9415637ae7b128391caacc17d18 /Lib/unittest.py | |
parent | f7a6b508ceee3eb614f9fa8ea89d9fbf86e84f9e (diff) | |
download | cpython-d2397753ee3d20579aa60b5e1c037e9e20db7ccb.zip cpython-d2397753ee3d20579aa60b5e1c037e9e20db7ccb.tar.gz cpython-d2397753ee3d20579aa60b5e1c037e9e20db7ccb.tar.bz2 |
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
........
Diffstat (limited to 'Lib/unittest.py')
-rw-r--r-- | Lib/unittest.py | 195 |
1 files changed, 186 insertions, 9 deletions
diff --git a/Lib/unittest.py b/Lib/unittest.py index 9c3024a..ce990b7 100644 --- a/Lib/unittest.py +++ b/Lib/unittest.py @@ -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,8 +1328,98 @@ 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) |