623 lines
25 KiB
Python
623 lines
25 KiB
Python
"""
|
|
Test Loader
|
|
-----------
|
|
|
|
nose's test loader implements the same basic functionality as its
|
|
superclass, unittest.TestLoader, but extends it by more liberal
|
|
interpretations of what may be a test and how a test may be named.
|
|
"""
|
|
|
|
|
|
import logging
|
|
import os
|
|
import sys
|
|
import unittest
|
|
import types
|
|
from inspect import isfunction
|
|
from nose.pyversion import unbound_method, ismethod
|
|
from nose.case import FunctionTestCase, MethodTestCase
|
|
from nose.failure import Failure
|
|
from nose.config import Config
|
|
from nose.importer import Importer, add_path, remove_path
|
|
from nose.selector import defaultSelector, TestAddress
|
|
from nose.util import func_lineno, getpackage, isclass, isgenerator, \
|
|
ispackage, regex_last_key, resolve_name, transplant_func, \
|
|
transplant_class, test_address
|
|
from nose.suite import ContextSuiteFactory, ContextList, LazySuite
|
|
from nose.pyversion import sort_list, cmp_to_key
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
#log.setLevel(logging.DEBUG)
|
|
|
|
# for efficiency and easier mocking
|
|
op_normpath = os.path.normpath
|
|
op_abspath = os.path.abspath
|
|
op_join = os.path.join
|
|
op_isdir = os.path.isdir
|
|
op_isfile = os.path.isfile
|
|
|
|
|
|
__all__ = ['TestLoader', 'defaultTestLoader']
|
|
|
|
|
|
class TestLoader(unittest.TestLoader):
|
|
"""Test loader that extends unittest.TestLoader to:
|
|
|
|
* Load tests from test-like functions and classes that are not
|
|
unittest.TestCase subclasses
|
|
* Find and load test modules in a directory
|
|
* Support tests that are generators
|
|
* Support easy extensions of or changes to that behavior through plugins
|
|
"""
|
|
config = None
|
|
importer = None
|
|
workingDir = None
|
|
selector = None
|
|
suiteClass = None
|
|
|
|
def __init__(self, config=None, importer=None, workingDir=None,
|
|
selector=None):
|
|
"""Initialize a test loader.
|
|
|
|
Parameters (all optional):
|
|
|
|
* config: provide a `nose.config.Config`_ or other config class
|
|
instance; if not provided a `nose.config.Config`_ with
|
|
default values is used.
|
|
* importer: provide an importer instance that implements
|
|
`importFromPath`. If not provided, a
|
|
`nose.importer.Importer`_ is used.
|
|
* workingDir: the directory to which file and module names are
|
|
relative. If not provided, assumed to be the current working
|
|
directory.
|
|
* selector: a selector class or instance. If a class is
|
|
provided, it will be instantiated with one argument, the
|
|
current config. If not provided, a `nose.selector.Selector`_
|
|
is used.
|
|
"""
|
|
if config is None:
|
|
config = Config()
|
|
if importer is None:
|
|
importer = Importer(config=config)
|
|
if workingDir is None:
|
|
workingDir = config.workingDir
|
|
if selector is None:
|
|
selector = defaultSelector(config)
|
|
elif isclass(selector):
|
|
selector = selector(config)
|
|
self.config = config
|
|
self.importer = importer
|
|
self.workingDir = op_normpath(op_abspath(workingDir))
|
|
self.selector = selector
|
|
if config.addPaths:
|
|
add_path(workingDir, config)
|
|
self.suiteClass = ContextSuiteFactory(config=config)
|
|
|
|
self._visitedPaths = set([])
|
|
|
|
unittest.TestLoader.__init__(self)
|
|
|
|
def getTestCaseNames(self, testCaseClass):
|
|
"""Override to select with selector, unless
|
|
config.getTestCaseNamesCompat is True
|
|
"""
|
|
if self.config.getTestCaseNamesCompat:
|
|
return unittest.TestLoader.getTestCaseNames(self, testCaseClass)
|
|
|
|
def wanted(attr, cls=testCaseClass, sel=self.selector):
|
|
item = getattr(cls, attr, None)
|
|
if isfunction(item):
|
|
item = unbound_method(cls, item)
|
|
elif not ismethod(item):
|
|
return False
|
|
return sel.wantMethod(item)
|
|
|
|
cases = list(filter(wanted, dir(testCaseClass)))
|
|
|
|
# add runTest if nothing else picked
|
|
if not cases and hasattr(testCaseClass, 'runTest'):
|
|
cases = ['runTest']
|
|
if self.sortTestMethodsUsing:
|
|
sort_list(cases, cmp_to_key(self.sortTestMethodsUsing))
|
|
return cases
|
|
|
|
def _haveVisited(self, path):
|
|
# For cases where path is None, we always pretend we haven't visited
|
|
# them.
|
|
if path is None:
|
|
return False
|
|
|
|
return path in self._visitedPaths
|
|
|
|
def _addVisitedPath(self, path):
|
|
if path is not None:
|
|
self._visitedPaths.add(path)
|
|
|
|
def loadTestsFromDir(self, path):
|
|
"""Load tests from the directory at path. This is a generator
|
|
-- each suite of tests from a module or other file is yielded
|
|
and is expected to be executed before the next file is
|
|
examined.
|
|
"""
|
|
log.debug("load from dir %s", path)
|
|
plugins = self.config.plugins
|
|
plugins.beforeDirectory(path)
|
|
if self.config.addPaths:
|
|
paths_added = add_path(path, self.config)
|
|
|
|
entries = os.listdir(path)
|
|
sort_list(entries, regex_last_key(self.config.testMatch))
|
|
for entry in entries:
|
|
# this hard-coded initial-dot test will be removed:
|
|
# http://code.google.com/p/python-nose/issues/detail?id=82
|
|
if entry.startswith('.'):
|
|
continue
|
|
entry_path = op_abspath(op_join(path, entry))
|
|
is_file = op_isfile(entry_path)
|
|
wanted = False
|
|
if is_file:
|
|
is_dir = False
|
|
wanted = self.selector.wantFile(entry_path)
|
|
else:
|
|
is_dir = op_isdir(entry_path)
|
|
if is_dir:
|
|
# this hard-coded initial-underscore test will be removed:
|
|
# http://code.google.com/p/python-nose/issues/detail?id=82
|
|
if entry.startswith('_'):
|
|
continue
|
|
wanted = self.selector.wantDirectory(entry_path)
|
|
is_package = ispackage(entry_path)
|
|
|
|
# Python 3.3 now implements PEP 420: Implicit Namespace Packages.
|
|
# As a result, it's now possible that parent paths that have a
|
|
# segment with the same basename as our package ends up
|
|
# in module.__path__. So we have to keep track of what we've
|
|
# visited, and not-revisit them again.
|
|
if wanted and not self._haveVisited(entry_path):
|
|
self._addVisitedPath(entry_path)
|
|
if is_file:
|
|
plugins.beforeContext()
|
|
if entry.endswith('.py'):
|
|
yield self.loadTestsFromName(
|
|
entry_path, discovered=True)
|
|
else:
|
|
yield self.loadTestsFromFile(entry_path)
|
|
plugins.afterContext()
|
|
elif is_package:
|
|
# Load the entry as a package: given the full path,
|
|
# loadTestsFromName() will figure it out
|
|
yield self.loadTestsFromName(
|
|
entry_path, discovered=True)
|
|
else:
|
|
# Another test dir in this one: recurse lazily
|
|
yield self.suiteClass(
|
|
lambda: self.loadTestsFromDir(entry_path))
|
|
tests = []
|
|
for test in plugins.loadTestsFromDir(path):
|
|
tests.append(test)
|
|
# TODO: is this try/except needed?
|
|
try:
|
|
if tests:
|
|
yield self.suiteClass(tests)
|
|
except (KeyboardInterrupt, SystemExit):
|
|
raise
|
|
except:
|
|
yield self.suiteClass([Failure(*sys.exc_info())])
|
|
|
|
# pop paths
|
|
if self.config.addPaths:
|
|
for p in paths_added:
|
|
remove_path(p)
|
|
plugins.afterDirectory(path)
|
|
|
|
def loadTestsFromFile(self, filename):
|
|
"""Load tests from a non-module file. Default is to raise a
|
|
ValueError; plugins may implement `loadTestsFromFile` to
|
|
provide a list of tests loaded from the file.
|
|
"""
|
|
log.debug("Load from non-module file %s", filename)
|
|
try:
|
|
tests = [test for test in
|
|
self.config.plugins.loadTestsFromFile(filename)]
|
|
if tests:
|
|
# Plugins can yield False to indicate that they were
|
|
# unable to load tests from a file, but it was not an
|
|
# error -- the file just had no tests to load.
|
|
tests = [_f for _f in tests if _f]
|
|
return self.suiteClass(tests)
|
|
else:
|
|
# Nothing was able to even try to load from this file
|
|
open(filename, 'r').close() # trigger os error
|
|
raise ValueError("Unable to load tests from file %s"
|
|
% filename)
|
|
except (KeyboardInterrupt, SystemExit):
|
|
raise
|
|
except:
|
|
exc = sys.exc_info()
|
|
return self.suiteClass(
|
|
[Failure(exc[0], exc[1], exc[2],
|
|
address=(filename, None, None))])
|
|
|
|
def loadTestsFromGenerator(self, generator, module):
|
|
"""Lazy-load tests from a generator function. The generator function
|
|
may yield either:
|
|
|
|
* a callable, or
|
|
* a function name resolvable within the same module
|
|
"""
|
|
def generate(g=generator, m=module):
|
|
try:
|
|
for test in g():
|
|
test_func, arg = self.parseGeneratedTest(test)
|
|
if not callable(test_func):
|
|
test_func = getattr(m, test_func)
|
|
yield FunctionTestCase(test_func, arg=arg, descriptor=g)
|
|
except KeyboardInterrupt:
|
|
raise
|
|
except:
|
|
exc = sys.exc_info()
|
|
yield Failure(exc[0], exc[1], exc[2],
|
|
address=test_address(generator))
|
|
return self.suiteClass(generate, context=generator, can_split=False)
|
|
|
|
def loadTestsFromGeneratorMethod(self, generator, cls):
|
|
"""Lazy-load tests from a generator method.
|
|
|
|
This is more complicated than loading from a generator function,
|
|
since a generator method may yield:
|
|
|
|
* a function
|
|
* a bound or unbound method, or
|
|
* a method name
|
|
"""
|
|
# convert the unbound generator method
|
|
# into a bound method so it can be called below
|
|
if hasattr(generator, 'im_class'):
|
|
cls = generator.__self__.__class__
|
|
inst = cls()
|
|
method = generator.__name__
|
|
generator = getattr(inst, method)
|
|
|
|
def generate(g=generator, c=cls):
|
|
try:
|
|
for test in g():
|
|
test_func, arg = self.parseGeneratedTest(test)
|
|
if not callable(test_func):
|
|
test_func = unbound_method(c, getattr(c, test_func))
|
|
if ismethod(test_func):
|
|
yield MethodTestCase(test_func, arg=arg, descriptor=g)
|
|
elif callable(test_func):
|
|
# In this case we're forcing the 'MethodTestCase'
|
|
# to run the inline function as its test call,
|
|
# but using the generator method as the 'method of
|
|
# record' (so no need to pass it as the descriptor)
|
|
yield MethodTestCase(g, test=test_func, arg=arg)
|
|
else:
|
|
yield Failure(
|
|
TypeError,
|
|
"%s is not a callable or method" % test_func)
|
|
except KeyboardInterrupt:
|
|
raise
|
|
except:
|
|
exc = sys.exc_info()
|
|
yield Failure(exc[0], exc[1], exc[2],
|
|
address=test_address(generator))
|
|
return self.suiteClass(generate, context=generator, can_split=False)
|
|
|
|
def loadTestsFromModule(self, module, path=None, discovered=False):
|
|
"""Load all tests from module and return a suite containing
|
|
them. If the module has been discovered and is not test-like,
|
|
the suite will be empty by default, though plugins may add
|
|
their own tests.
|
|
"""
|
|
log.debug("Load from module %s", module)
|
|
tests = []
|
|
test_classes = []
|
|
test_funcs = []
|
|
# For *discovered* modules, we only load tests when the module looks
|
|
# testlike. For modules we've been directed to load, we always
|
|
# look for tests. (discovered is set to True by loadTestsFromDir)
|
|
if not discovered or self.selector.wantModule(module):
|
|
for item in dir(module):
|
|
test = getattr(module, item, None)
|
|
# print "Check %s (%s) in %s" % (item, test, module.__name__)
|
|
if isclass(test):
|
|
if self.selector.wantClass(test):
|
|
test_classes.append(test)
|
|
elif isfunction(test) and self.selector.wantFunction(test):
|
|
test_funcs.append(test)
|
|
sort_list(test_classes, lambda x: x.__name__)
|
|
sort_list(test_funcs, func_lineno)
|
|
tests = [self.makeTest(t, parent=module) for t in test_classes + test_funcs]
|
|
|
|
# Now, descend into packages
|
|
# FIXME can or should this be lazy?
|
|
# is this syntax 2.2 compatible?
|
|
module_paths = getattr(module, '__path__', [])
|
|
|
|
if path:
|
|
path = os.path.normcase(os.path.realpath(path))
|
|
|
|
for module_path in module_paths:
|
|
log.debug("Load tests from module path %s?", module_path)
|
|
log.debug("path: %s os.path.realpath(%s): %s",
|
|
path, os.path.normcase(module_path),
|
|
os.path.realpath(os.path.normcase(module_path)))
|
|
if (self.config.traverseNamespace or not path) or \
|
|
os.path.realpath(
|
|
os.path.normcase(module_path)).startswith(path):
|
|
# Egg files can be on sys.path, so make sure the path is a
|
|
# directory before trying to load from it.
|
|
if os.path.isdir(module_path):
|
|
tests.extend(self.loadTestsFromDir(module_path))
|
|
|
|
for test in self.config.plugins.loadTestsFromModule(module, path):
|
|
tests.append(test)
|
|
|
|
return self.suiteClass(ContextList(tests, context=module))
|
|
|
|
def loadTestsFromName(self, name, module=None, discovered=False):
|
|
"""Load tests from the entity with the given name.
|
|
|
|
The name may indicate a file, directory, module, or any object
|
|
within a module. See `nose.util.split_test_name` for details on
|
|
test name parsing.
|
|
"""
|
|
# FIXME refactor this method into little bites?
|
|
log.debug("load from %s (%s)", name, module)
|
|
|
|
suite = self.suiteClass
|
|
|
|
# give plugins first crack
|
|
plug_tests = self.config.plugins.loadTestsFromName(name, module)
|
|
if plug_tests:
|
|
return suite(plug_tests)
|
|
|
|
addr = TestAddress(name, workingDir=self.workingDir)
|
|
if module:
|
|
# Two cases:
|
|
# name is class.foo
|
|
# The addr will be incorrect, since it thinks class.foo is
|
|
# a dotted module name. It's actually a dotted attribute
|
|
# name. In this case we want to use the full submitted
|
|
# name as the name to load from the module.
|
|
# name is module:class.foo
|
|
# The addr will be correct. The part we want is the part after
|
|
# the :, which is in addr.call.
|
|
if addr.call:
|
|
name = addr.call
|
|
parent, obj = self.resolve(name, module)
|
|
if (isclass(parent)
|
|
and getattr(parent, '__module__', None) != module.__name__
|
|
and not isinstance(obj, Failure)):
|
|
parent = transplant_class(parent, module.__name__)
|
|
obj = getattr(parent, obj.__name__)
|
|
log.debug("parent %s obj %s module %s", parent, obj, module)
|
|
if isinstance(obj, Failure):
|
|
return suite([obj])
|
|
else:
|
|
return suite(ContextList([self.makeTest(obj, parent)],
|
|
context=parent))
|
|
else:
|
|
if addr.module:
|
|
try:
|
|
if addr.filename is None:
|
|
module = resolve_name(addr.module)
|
|
else:
|
|
self.config.plugins.beforeImport(
|
|
addr.filename, addr.module)
|
|
# FIXME: to support module.name names,
|
|
# do what resolve-name does and keep trying to
|
|
# import, popping tail of module into addr.call,
|
|
# until we either get an import or run out of
|
|
# module parts
|
|
try:
|
|
module = self.importer.importFromPath(
|
|
addr.filename, addr.module)
|
|
finally:
|
|
self.config.plugins.afterImport(
|
|
addr.filename, addr.module)
|
|
except (KeyboardInterrupt, SystemExit):
|
|
raise
|
|
except:
|
|
exc = sys.exc_info()
|
|
return suite([Failure(exc[0], exc[1], exc[2],
|
|
address=addr.totuple())])
|
|
if addr.call:
|
|
return self.loadTestsFromName(addr.call, module)
|
|
else:
|
|
return self.loadTestsFromModule(
|
|
module, addr.filename,
|
|
discovered=discovered)
|
|
elif addr.filename:
|
|
path = addr.filename
|
|
if addr.call:
|
|
package = getpackage(path)
|
|
if package is None:
|
|
return suite([
|
|
Failure(ValueError,
|
|
"Can't find callable %s in file %s: "
|
|
"file is not a python module" %
|
|
(addr.call, path),
|
|
address=addr.totuple())])
|
|
return self.loadTestsFromName(addr.call, module=package)
|
|
else:
|
|
if op_isdir(path):
|
|
# In this case we *can* be lazy since we know
|
|
# that each module in the dir will be fully
|
|
# loaded before its tests are executed; we
|
|
# also know that we're not going to be asked
|
|
# to load from . and ./some_module.py *as part
|
|
# of this named test load*
|
|
return LazySuite(
|
|
lambda: self.loadTestsFromDir(path))
|
|
elif op_isfile(path):
|
|
return self.loadTestsFromFile(path)
|
|
else:
|
|
return suite([
|
|
Failure(OSError, "No such file %s" % path,
|
|
address=addr.totuple())])
|
|
else:
|
|
# just a function? what to do? I think it can only be
|
|
# handled when module is not None
|
|
return suite([
|
|
Failure(ValueError, "Unresolvable test name %s" % name,
|
|
address=addr.totuple())])
|
|
|
|
def loadTestsFromNames(self, names, module=None):
|
|
"""Load tests from all names, returning a suite containing all
|
|
tests.
|
|
"""
|
|
plug_res = self.config.plugins.loadTestsFromNames(names, module)
|
|
if plug_res:
|
|
suite, names = plug_res
|
|
if suite:
|
|
return self.suiteClass([
|
|
self.suiteClass(suite),
|
|
unittest.TestLoader.loadTestsFromNames(self, names, module)
|
|
])
|
|
return unittest.TestLoader.loadTestsFromNames(self, names, module)
|
|
|
|
def loadTestsFromTestCase(self, testCaseClass):
|
|
"""Load tests from a unittest.TestCase subclass.
|
|
"""
|
|
cases = []
|
|
plugins = self.config.plugins
|
|
for case in plugins.loadTestsFromTestCase(testCaseClass):
|
|
cases.append(case)
|
|
# For efficiency in the most common case, just call and return from
|
|
# super. This avoids having to extract cases and rebuild a context
|
|
# suite when there are no plugin-contributed cases.
|
|
if not cases:
|
|
return super(TestLoader, self).loadTestsFromTestCase(testCaseClass)
|
|
cases.extend(
|
|
[case for case in
|
|
super(TestLoader, self).loadTestsFromTestCase(testCaseClass)])
|
|
return self.suiteClass(cases)
|
|
|
|
def loadTestsFromTestClass(self, cls):
|
|
"""Load tests from a test class that is *not* a unittest.TestCase
|
|
subclass.
|
|
|
|
In this case, we can't depend on the class's `__init__` taking method
|
|
name arguments, so we have to compose a MethodTestCase for each
|
|
method in the class that looks testlike.
|
|
"""
|
|
def wanted(attr, cls=cls, sel=self.selector):
|
|
item = getattr(cls, attr, None)
|
|
if isfunction(item):
|
|
item = unbound_method(cls, item)
|
|
elif not ismethod(item):
|
|
return False
|
|
return sel.wantMethod(item)
|
|
cases = [self.makeTest(getattr(cls, case), cls)
|
|
for case in filter(wanted, dir(cls))]
|
|
for test in self.config.plugins.loadTestsFromTestClass(cls):
|
|
cases.append(test)
|
|
return self.suiteClass(ContextList(cases, context=cls))
|
|
|
|
def makeTest(self, obj, parent=None):
|
|
try:
|
|
return self._makeTest(obj, parent)
|
|
except (KeyboardInterrupt, SystemExit):
|
|
raise
|
|
except:
|
|
exc = sys.exc_info()
|
|
try:
|
|
addr = test_address(obj)
|
|
except KeyboardInterrupt:
|
|
raise
|
|
except:
|
|
addr = None
|
|
return Failure(exc[0], exc[1], exc[2], address=addr)
|
|
|
|
def _makeTest(self, obj, parent=None):
|
|
"""Given a test object and its parent, return a test case
|
|
or test suite.
|
|
"""
|
|
plug_tests = []
|
|
try:
|
|
addr = test_address(obj)
|
|
except KeyboardInterrupt:
|
|
raise
|
|
except:
|
|
addr = None
|
|
for test in self.config.plugins.makeTest(obj, parent):
|
|
plug_tests.append(test)
|
|
# TODO: is this try/except needed?
|
|
try:
|
|
if plug_tests:
|
|
return self.suiteClass(plug_tests)
|
|
except (KeyboardInterrupt, SystemExit):
|
|
raise
|
|
except:
|
|
exc = sys.exc_info()
|
|
return Failure(exc[0], exc[1], exc[2], address=addr)
|
|
|
|
if isfunction(obj) and parent and not isinstance(parent, types.ModuleType):
|
|
# This is a Python 3.x 'unbound method'. Wrap it with its
|
|
# associated class..
|
|
obj = unbound_method(parent, obj)
|
|
|
|
if isinstance(obj, unittest.TestCase):
|
|
return obj
|
|
elif isclass(obj):
|
|
if parent and obj.__module__ != parent.__name__:
|
|
obj = transplant_class(obj, parent.__name__)
|
|
if issubclass(obj, unittest.TestCase):
|
|
return self.loadTestsFromTestCase(obj)
|
|
else:
|
|
return self.loadTestsFromTestClass(obj)
|
|
elif ismethod(obj):
|
|
if parent is None:
|
|
parent = obj.__class__
|
|
if issubclass(parent, unittest.TestCase):
|
|
return parent(obj.__name__)
|
|
else:
|
|
if isgenerator(obj):
|
|
return self.loadTestsFromGeneratorMethod(obj, parent)
|
|
else:
|
|
return MethodTestCase(obj)
|
|
elif isfunction(obj):
|
|
if parent and obj.__module__ != parent.__name__:
|
|
obj = transplant_func(obj, parent.__name__)
|
|
if isgenerator(obj):
|
|
return self.loadTestsFromGenerator(obj, parent)
|
|
else:
|
|
return FunctionTestCase(obj)
|
|
else:
|
|
return Failure(TypeError,
|
|
"Can't make a test from %s" % obj,
|
|
address=addr)
|
|
|
|
def resolve(self, name, module):
|
|
"""Resolve name within module
|
|
"""
|
|
obj = module
|
|
parts = name.split('.')
|
|
for part in parts:
|
|
parent, obj = obj, getattr(obj, part, None)
|
|
if obj is None:
|
|
# no such test
|
|
obj = Failure(ValueError, "No such test %s" % name)
|
|
return parent, obj
|
|
|
|
def parseGeneratedTest(self, test):
|
|
"""Given the yield value of a test generator, return a func and args.
|
|
|
|
This is used in the two loadTestsFromGenerator* methods.
|
|
|
|
"""
|
|
if not isinstance(test, tuple): # yield test
|
|
test_func, arg = (test, tuple())
|
|
elif len(test) == 1: # yield (test,)
|
|
test_func, arg = (test[0], tuple())
|
|
else: # yield test, foo, bar, ...
|
|
assert len(test) > 1 # sanity check
|
|
test_func, arg = (test[0], test[1:])
|
|
return test_func, arg
|
|
|
|
defaultTestLoader = TestLoader
|
|
|