"""
Discovery-based test loader.
This plugin implements nose2's automatic test module discovery. It
looks for test modules in packages and directories whose names start
with ``test``, then fires the :func:`loadTestsFromModule` hook for each
one to allow other plugins to load the actual tests.
It also fires :func:`handleFile` for every file that it sees, and
:func:`matchPath` for every Python module, to allow other plugins to
load tests from other kinds of files and to influence which modules
are examined for tests.
"""
# Adapted from unittest2/loader.py from the unittest2 plugins branch.
# This module contains some code copied from unittest2/loader.py and other
# code developed in reference to that module and others within unittest2.
# unittest2 is Copyright (c) 2001-2010 Python Software Foundation; All
# Rights Reserved. See: http://docs.python.org/license.html
import logging
import os
import sys
from fnmatch import fnmatch
from nose2 import events, util
__unittest = True
log = logging.getLogger(__name__)
class DirectoryHandler(object):
def __init__(self, session):
self.session = session
self.event_handled = False
def handle_dir(self, event, full_path, top_level):
dirname = os.path.basename(full_path)
pattern = self.session.testFilePattern
evt = events.HandleFileEvent(
event.loader, dirname, full_path, pattern, top_level
)
result = self.session.hooks.handleDir(evt)
if evt.extraTests:
for test in evt.extraTests:
yield test
if evt.handled:
if result:
yield result
self.event_handled = True
return
evt = events.MatchPathEvent(dirname, full_path, pattern)
result = self.session.hooks.matchDirPath(evt)
if evt.handled and not result:
self.event_handled = True
class Discoverer(object):
def loadTestsFromName(self, event):
"""Load tests from module named by event.name"""
# turn name into path or module name
# fire appropriate hooks (handle file or load from module)
if event.module:
return
name = event.name
module = None
_, top_level_dir = self._getStartDirs()
try:
# try name as a dotted module name first
__import__(name)
module = sys.modules[name]
except (KeyboardInterrupt, SystemExit):
raise
except BaseException:
# if that fails, try it as a file or directory
event.extraTests.extend(self._find_tests(event, name, top_level_dir))
else:
event.extraTests.extend(
self._find_tests_in_module(event, module, top_level_dir)
)
def loadTestsFromNames(self, event):
"""Discover tests if no test names specified"""
log.debug("Received event %s", event)
if event.names or event.module:
return
event.handled = True # I will handle discovery
return self._discover(event)
def _checkIfPathIsOK(self, start_dir):
if not os.path.isdir(os.path.abspath(start_dir)):
raise OSError("%s is not a directory" % os.path.abspath(start_dir))
def _getStartDirs(self):
start_dir = self.session.startDir
top_level_dir = self.session.topLevelDir
if start_dir is None:
start_dir = "."
if top_level_dir is None:
top_level_dir = start_dir
self._checkIfPathIsOK(start_dir)
is_not_importable = False
start_dir = os.path.abspath(start_dir)
top_level_dir = os.path.abspath(top_level_dir)
if start_dir != top_level_dir:
is_not_importable = not os.path.isfile(
os.path.join(start_dir, "__init__.py")
)
if is_not_importable:
raise ImportError("Start directory is not importable: %r" % start_dir)
# this is redundant in some cases, but that's ok
self.session.prepareSysPath()
return start_dir, top_level_dir
def _discover(self, event):
loader = event.loader
try:
start_dir, top_level_dir = self._getStartDirs()
except (OSError, ImportError):
return loader.suiteClass(
loader.failedLoadTests(self.session.startDir, sys.exc_info())
)
log.debug("_discover in %s (%s)", start_dir, top_level_dir)
tests = list(self._find_tests(event, start_dir, top_level_dir))
return loader.suiteClass(tests)
def _find_tests(self, event, start, top_level):
"""Used by discovery. Yields test suites it loads."""
log.debug("_find_tests(%r, %r)", start, top_level)
if start == top_level:
full_path = start
else:
full_path = os.path.join(top_level, start)
if os.path.isdir(start):
for test in self._find_tests_in_dir(event, full_path, top_level):
yield test
elif os.path.isfile(start):
for test in self._find_tests_in_file(event, start, full_path, top_level):
yield test
def _find_tests_in_dir(self, event, full_path, top_level):
if not os.path.isdir(full_path):
return
log.debug("find in dir %s (%s)", full_path, top_level)
dir_handler = DirectoryHandler(self.session)
for test in dir_handler.handle_dir(event, full_path, top_level):
yield test
if dir_handler.event_handled:
return
for path in os.listdir(full_path):
entry_path = os.path.join(full_path, path)
if os.path.isfile(entry_path):
for test in self._find_tests_in_file(
event, path, entry_path, top_level
):
yield test
elif os.path.isdir(entry_path):
if (
"test" in path.lower()
or util.ispackage(entry_path)
or path in self.session.libDirs
):
for test in self._find_tests(event, entry_path, top_level):
yield test
def _find_tests_in_file(
self, event, filename, full_path, top_level, module_name=None
):
log.debug("find in file %s (%s)", full_path, top_level)
pattern = self.session.testFilePattern
loader = event.loader
evt = events.HandleFileEvent(loader, filename, full_path, pattern, top_level)
result = self.session.hooks.handleFile(evt)
if evt.extraTests:
yield loader.suiteClass(evt.extraTests)
if evt.handled:
if result:
yield result
return
if not util.valid_module_name(filename):
# valid Python identifiers only
return
evt = events.MatchPathEvent(filename, full_path, pattern)
result = self.session.hooks.matchPath(evt)
if evt.handled:
if not result:
return
elif not self._match_path(filename, full_path, pattern):
return
if module_name is None:
module_name, package_path = util.name_from_path(full_path)
util.ensure_importable(package_path)
try:
module = util.module_from_name(module_name)
except BaseException:
yield loader.failedImport(module_name)
else:
mod_file = os.path.abspath(getattr(module, "__file__", full_path))
realpath = os.path.splitext(mod_file)[0]
fullpath_noext = os.path.splitext(full_path)[0]
if realpath.lower() != fullpath_noext.lower():
module_dir = os.path.dirname(realpath)
mod_name = os.path.splitext(os.path.basename(full_path))[0]
expected_dir = os.path.dirname(full_path)
msg = (
"%r module incorrectly imported from %r. "
"Expected %r. Is this module globally installed?"
)
raise ImportError(msg % (mod_name, module_dir, expected_dir))
yield loader.loadTestsFromModule(module)
def _find_tests_in_module(self, event, module, top_level_dir):
# only called from loadTestsFromName
yield event.loader.loadTestsFromModule(module)
# may be a package; recurse into __path__ if so
pkgpath = getattr(module, "__path__", None)
if pkgpath:
for entry in pkgpath:
full_path = os.path.abspath(os.path.join(top_level_dir, entry))
for test in self._find_tests_in_dir(event, full_path, top_level_dir):
yield test
def _match_path(self, path, full_path, pattern):
# override this method to use alternative matching strategy
return fnmatch(path, pattern)
[docs]class DiscoveryLoader(events.Plugin, Discoverer):
"""Loader plugin that can discover tests"""
alwaysOn = True
configSection = "discovery"
def registerInSubprocess(self, event):
event.pluginClasses.append(self.__class__)
[docs] def loadTestsFromName(self, event):
"""Load tests from module named by event.name"""
return Discoverer.loadTestsFromName(self, event)
[docs] def loadTestsFromNames(self, event):
"""Discover tests if no test names specified"""
return Discoverer.loadTestsFromNames(self, event)