-
Notifications
You must be signed in to change notification settings - Fork 51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support unittesting via 'python setup.py test' #193
Changes from all commits
7213bfd
480b2d0
96bac0e
64cdfee
dc2f184
571edb3
18ed55a
7714e17
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,30 +24,75 @@ | |
import shutil | ||
import sys | ||
import re | ||
|
||
from distutils import log # also for setuptools | ||
from distutils.command.bdist_rpm import bdist_rpm as orig_bdist_rpm | ||
from distutils.dir_util import remove_tree | ||
|
||
from setuptools.command.test import test as TestCommand | ||
from setuptools.command.test import ScanningLoader | ||
from setuptools import setup | ||
from setuptools.command.bdist_rpm import bdist_rpm | ||
from setuptools.command.build_py import build_py | ||
from setuptools.command.egg_info import egg_info | ||
from setuptools.command.install_scripts import install_scripts | ||
# egg_info uses sdist directly through manifest_maker | ||
from setuptools.command.sdist import sdist | ||
|
||
from unittest import TestSuite | ||
|
||
# private class variables to communicate | ||
# between VscScanningLoader and VscTestCommand | ||
# stored in __builtin__ because the (Vsc)TestCommand.run_tests | ||
# reloads and cleans up the modules | ||
import __builtin__ | ||
if not hasattr(__builtin__,'__test_filter'): | ||
setattr(__builtin__, '__test_filter', { | ||
'module': None, | ||
'function': None, | ||
'allowmods': [], | ||
}) | ||
|
||
# Keep this for legacy reasons, setuptools didn't used to be a requirement | ||
has_setuptools = True | ||
|
||
# 0 : WARN (default), 1 : INFO, 2 : DEBUG | ||
log.set_verbosity(2) | ||
|
||
has_setuptools = None | ||
# available authors | ||
ag = ('Andy Georges', 'andy.georges@ugent.be') | ||
eh = ('Ewan Higgs', 'Ewan.Higgs@UGent.be') | ||
jt = ('Jens Timmermans', 'jens.timmermans@ugent.be') | ||
kh = ('Kenneth Hoste', 'kenneth.hoste@ugent.be') | ||
kw = ('Kenneth Waegeman', 'Kenneth.Waegeman@UGent.be') | ||
lm = ('Luis Fernando Munoz Meji?as', 'luis.munoz@ugent.be') | ||
sdw = ('Stijn De Weirdt', 'stijn.deweirdt@ugent.be') | ||
wdp = ('Wouter Depypere', 'wouter.depypere@ugent.be') | ||
wp = ('Ward Poelmans', 'Ward.Poelmans@UGent.be') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe keep them ordered alphabetically? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, are you using a different alphabet than the rest of us? ;) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are you? (and are you suggestion emacs' sort-fields is wrong 😄 ) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. some alphabet, different sorting key (variable name vs actual name of person), so nvm :) |
||
|
||
# Regexp used to remove suffixes from scripts when installing(/packaging) | ||
REGEXP_REMOVE_SUFFIX = re.compile(r'(\.(?:py|sh|pl))$') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do I dare ask about this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i added a proper comment |
||
|
||
# We do need all setup files to be included in the source dir if we ever want to install | ||
# the package elsewhere. | ||
# We do need all setup files to be included in the source dir | ||
# if we ever want to install the package elsewhere. | ||
EXTRA_SDIST_FILES = ['setup.py'] | ||
|
||
# Put unittests under this directory | ||
DEFAULT_TEST_SUITE = 'test' | ||
|
||
URL_GH_HPCUGENT = 'https://github.com/hpcugent/%(name)s' | ||
URL_GHUGENT_HPCUGENT = 'https://github.ugent.be/hpcugent/%(name)s' | ||
|
||
|
||
def find_extra_sdist_files(): | ||
"""Looks for files to append to the FileList that is used by the egg_info.""" | ||
print "looking for extra dist files" | ||
log.info("looking for extra dist files") | ||
filelist = [] | ||
for fn in EXTRA_SDIST_FILES: | ||
if os.path.isfile(fn): | ||
filelist.append(fn) | ||
else: | ||
print "sdist add_defaults Failed to find %s" % fn | ||
print "exiting." | ||
log.error("sdist add_defaults Failed to find %s. Exiting." % fn) | ||
sys.exit(1) | ||
return filelist | ||
|
||
|
@@ -61,80 +106,35 @@ def remove_extra_bdist_rpm_files(): | |
""" | ||
return [] | ||
|
||
# The following aims to import from setuptools, but if this is not available, we import the basic functionality from | ||
# distutils instead. Note that setuptools make copies of the scripts, it does _not_ preserve symbolic links. | ||
try: | ||
#raise ImportError("no setuptools") # to try distutils, uncomment | ||
from setuptools import setup | ||
from setuptools.command.bdist_rpm import bdist_rpm | ||
from distutils.command.bdist_rpm import bdist_rpm as orig_bdist_rpm | ||
from setuptools.command.build_py import build_py | ||
from setuptools.command.install_scripts import install_scripts | ||
from setuptools.command.sdist import sdist | ||
|
||
# egg_info uses sdist directly through manifest_maker | ||
from setuptools.command.egg_info import egg_info | ||
|
||
class vsc_egg_info(egg_info): | ||
"""Class to determine the set of files that should be included. | ||
|
||
This amounts to including the default files, as determined by setuptools, extended with the | ||
few extra files we need to add for installation purposes. | ||
""" | ||
|
||
def find_sources(self): | ||
"""Default lookup.""" | ||
egg_info.find_sources(self) | ||
self.filelist.extend(find_extra_sdist_files()) | ||
|
||
# TODO: this should be in the setup.py, here we should have a placeholder, so we need not change this for every | ||
# package we deploy | ||
class vsc_bdist_rpm_egg_info(vsc_egg_info): | ||
"""Class to determine the source files that should be present in an (S)RPM. | ||
|
||
All __init__.py files that augment namespace packages should be installed by the | ||
dependent package, so we need not install it here. | ||
""" | ||
|
||
def find_sources(self): | ||
"""Fins the sources as default and then drop the cruft.""" | ||
vsc_egg_info.find_sources(self) | ||
for f in remove_extra_bdist_rpm_files(): | ||
print "DEBUG: removing %s from source list" % (f) | ||
self.filelist.files.remove(f) | ||
|
||
has_setuptools = True | ||
except ImportError as err: | ||
log.warn("ImportError, falling back to distutils-only: %s", err) | ||
from distutils.core import setup | ||
from distutils.command.install_scripts import install_scripts | ||
from distutils.command.build_py import build_py | ||
from distutils.command.sdist import sdist | ||
from distutils.command.bdist_rpm import bdist_rpm as orig_bdist_rpm | ||
bdist_rpm = orig_bdist_rpm # mimic setuptools' bdist_rpm | ||
|
||
class vsc_egg_info(object): | ||
pass # dummy class for distutils | ||
class vsc_egg_info(egg_info): | ||
"""Class to determine the set of files that should be included. | ||
|
||
class vsc_bdist_rpm_egg_info(vsc_egg_info): | ||
pass # dummy class for distutils | ||
This amounts to including the default files, as determined by setuptools, extended with the | ||
few extra files we need to add for installation purposes. | ||
""" | ||
|
||
has_setuptools = False | ||
def find_sources(self): | ||
"""Default lookup.""" | ||
egg_info.find_sources(self) | ||
self.filelist.extend(find_extra_sdist_files()) | ||
|
||
# TODO: this should be in the setup.py, here we should have a placeholder, so we need not change this for every | ||
# package we deploy | ||
class vsc_bdist_rpm_egg_info(vsc_egg_info): | ||
"""Class to determine the source files that should be present in an (S)RPM. | ||
|
||
# available authors | ||
ag = ('Andy Georges', 'andy.georges@ugent.be') | ||
jt = ('Jens Timmermans', 'jens.timmermans@ugent.be') | ||
kh = ('Kenneth Hoste', 'kenneth.hoste@ugent.be') | ||
lm = ('Luis Fernando Munoz Meji?as', 'luis.munoz@ugent.be') | ||
sdw = ('Stijn De Weirdt', 'stijn.deweirdt@ugent.be') | ||
wdp = ('Wouter Depypere', 'wouter.depypere@ugent.be') | ||
kw = ('Kenneth Waegeman', 'Kenneth.Waegeman@UGent.be') | ||
eh = ('Ewan Higgs', 'Ewan.Higgs@UGent.be') | ||
All __init__.py files that augment namespace packages should be installed by the | ||
dependent package, so we need not install it here. | ||
""" | ||
|
||
def find_sources(self): | ||
"""Finds the sources as default and then drop the cruft.""" | ||
vsc_egg_info.find_sources(self) | ||
for fn in remove_extra_bdist_rpm_files(): | ||
log.debug("removing %s from source list" % (fn)) | ||
self.filelist.files.remove(fn) | ||
|
||
# FIXME: do we need this here? it won;t hurt, but still ... | ||
REGEXP_REMOVE_SUFFIX = re.compile(r'(\.(?:py|sh|pl))$') | ||
|
||
class vsc_install_scripts(install_scripts): | ||
"""Create the (fake) links for mympirun also remove .sh and .py extensions from the scripts.""" | ||
|
@@ -166,14 +166,168 @@ def find_package_modules (self, package, package_dir): | |
|
||
|
||
class vsc_bdist_rpm(bdist_rpm): | ||
""" Custom class to build the RPM, since the __inti__.py cannot be included for the packages that have namespace spread across all of the machine.""" | ||
"""Custom class to build the RPM, since the __init__.py cannot be included for the packages that have namespace spread across all of the machine.""" | ||
def run(self): | ||
log.error("vsc_bdist_rpm = %s" % (self.__dict__)) | ||
SHARED_TARGET['cmdclass']['egg_info'] = vsc_bdist_rpm_egg_info # changed to allow removal of files | ||
self.run_command('egg_info') # ensure distro name is up-to-date | ||
orig_bdist_rpm.run(self) | ||
|
||
|
||
def filter_testsuites(testsuites): | ||
"""(Recursive) filtering of (suites of) tests""" | ||
test_filter = getattr(__builtin__, '__test_filter')['function'] | ||
|
||
res = type(testsuites)() | ||
|
||
for ts in testsuites: | ||
# ts is either a test or testsuite of more tests | ||
if isinstance(ts, TestSuite): | ||
res.addTest(filter_testsuites(ts)) | ||
else: | ||
if re.search(test_filter, ts._testMethodName): | ||
res.addTest(ts) | ||
return res | ||
|
||
|
||
class VscScanningLoader(ScanningLoader): | ||
"""The class to look for tests""" | ||
|
||
TEST_LOADER_MODULE = __name__ | ||
|
||
def loadTestsFromModule(self, module): | ||
""" | ||
Support test module and function name based filtering | ||
""" | ||
testsuites = ScanningLoader.loadTestsFromModule(self, module) | ||
|
||
test_filter = getattr(__builtin__,'__test_filter') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. space after |
||
|
||
res = testsuites | ||
|
||
if test_filter['module'] is not None: | ||
name = module.__name__ | ||
if name in test_filter['allowmods']: | ||
# a parent name space | ||
pass | ||
elif re.search(test_filter['module'], name): | ||
if test_filter['function'] is not None: | ||
res = filter_testsuites(testsuites) | ||
# add parents (and module itself) | ||
pms = name.split('.') | ||
for pm_idx in range(len(pms)): | ||
pm = '.'.join(pms[:pm_idx]) | ||
if not pm in test_filter['allowmods']: | ||
test_filter['allowmods'].append(pm) | ||
else: | ||
res = type(testsuites)() | ||
|
||
return res | ||
|
||
|
||
class VscTestCommand(TestCommand): | ||
""" | ||
The cmdclass for testing | ||
""" | ||
|
||
# make 2 new 'python setup.py test' options available | ||
user_options = TestCommand.user_options + [ | ||
('test-filterf=', 'f', "Regex filter on test function names"), | ||
('test-filterm=', 'F', "Regex filter on test (sub)modules"), | ||
] | ||
|
||
TEST_LOADER_CLASS = VscScanningLoader | ||
|
||
def initialize_options(self): | ||
""" | ||
Add attributes for new commandline options and set test_loader | ||
""" | ||
TestCommand.initialize_options(self) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. missing docstring |
||
self.test_filterm = None | ||
self.test_filterf = None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what magic takes care of populating these if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. setuptools |
||
self.test_loader = '%s:%s' % (self.TEST_LOADER_CLASS.TEST_LOADER_MODULE, self.TEST_LOADER_CLASS.__name__) | ||
log.info("test_loader set to %s" % self.test_loader) | ||
|
||
def setup_sys_path(self): | ||
""" | ||
Prepare sys.path to be able to | ||
use the modules provided by this package (assumeing they are in 'lib') | ||
use any scripts as modules (for unittesting) | ||
use the test modules as modules (for unittesting) | ||
Returns a list of directories to cleanup | ||
""" | ||
cleanup = [] | ||
|
||
# determine the base directory of the repository | ||
# we will assume that the tests are called from | ||
# a 'setup.py' like file in the basedirectory | ||
# (but could be called anything, as long as it is in the basedir) | ||
setup_py = os.path.abspath(sys.argv[0]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you can verify whether it is, by checking There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm, what if someone uses it from a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On 09/10/15 09:48, Stijn De Weirdt wrote:
Then the check will fail. You can make it a bit more flexible if you os.path.basename(setup_py).startswith('setup') |
||
log.info('run_tests from %s' % setup_py) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we really need inspect here? it is kind of slow, and does not work if the .py file is not available (like when you only have the .pyc or pyo files)
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i pulled the inspect code form stackoverflow, rereading it, they tried to solve something more complicated, so i'll revert to |
||
base_dir = os.path.dirname(setup_py) | ||
|
||
# make a lib dir to trick setup.py to package this properly | ||
# and git ignore empty dirs, so recreate it if necessary | ||
lib_dir = os.path.join(base_dir, 'lib') | ||
if not os.path.exists(lib_dir): | ||
os.mkdir(lib_dir) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no cleanup? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no really, this is simply an empty directory that is missing because git doesn't handle empty dirs. |
||
cleanup.append(lib_dir) | ||
|
||
test_dir = os.path.join(base_dir, DEFAULT_TEST_SUITE) | ||
if os.path.isdir(test_dir): | ||
sys.path.insert(0, test_dir) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wait, shouldn't this add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no, should add testdir for modules in test dir that are no unittests but are used in unittests There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok |
||
else: | ||
raise Exception("Can't find location of testsuite directory %s in %s" % (DEFAULT_TEST_SUITE, base_dir)) | ||
|
||
# make sure we can import the script as a module | ||
scripts_dir = os.path.join(base_dir, 'bin') | ||
if os.path.isdir(scripts_dir): | ||
sys.path.insert(0, scripts_dir) | ||
|
||
return cleanup | ||
|
||
def reload_vsc_modules(self): | ||
""" | ||
Cleanup and restore vsc namespace becasue we use vsc namespace tools very early | ||
So we need to make sure they are picked up from the paths as specified | ||
in setup_sys_path, not to mix with installed and already loaded modules | ||
""" | ||
loaded_vsc_modules = [name for name in sys.modules.keys() if name == 'vsc' or name.startswith('vsc.')] | ||
reload_vsc_modules = [] | ||
for name in loaded_vsc_modules: | ||
if hasattr(sys.modules[name], '__file__'): | ||
# only actual modules | ||
reload_vsc_modules.append(name) | ||
del(sys.modules[name]) | ||
|
||
# reimport | ||
for name in reload_vsc_modules: | ||
__import__(name) | ||
|
||
def run_tests(self): | ||
""" | ||
Actually run the tests, but start with | ||
passing the filter options via __builtin__ | ||
set sys.path | ||
reload vsc modules | ||
""" | ||
getattr(__builtin__,'__test_filter').update({ | ||
'function': self.test_filterf, | ||
'module': self.test_filterm, | ||
}) | ||
|
||
cleanup = self.setup_sys_path() | ||
|
||
self.reload_vsc_modules() | ||
|
||
res = TestCommand.run_tests(self) | ||
|
||
# clenaup any diretcories created | ||
for directory in cleanup: | ||
shutil.rmtree(directory) | ||
|
||
return res | ||
|
||
# shared target config | ||
SHARED_TARGET = { | ||
'url': '', | ||
|
@@ -184,6 +338,8 @@ def run(self): | |
"egg_info": vsc_egg_info, | ||
"bdist_rpm": vsc_bdist_rpm, | ||
}, | ||
'cmdclass': {'test': VscTestCommand}, | ||
'test_suite': DEFAULT_TEST_SUITE, | ||
} | ||
|
||
|
||
|
@@ -265,7 +421,7 @@ def build_setup_cfg_for_bdist_rpm(target): | |
setup_cfg.close() | ||
|
||
|
||
def action_target(target, setupfn=setup, extra_sdist=[]): | ||
def action_target(target, setupfn=setup, extra_sdist=[], urltemplate=None): | ||
# EXTRA_SDIST_FILES.extend(extra_sdist) | ||
|
||
do_cleanup = True | ||
|
@@ -283,6 +439,11 @@ def action_target(target, setupfn=setup, extra_sdist=[]): | |
if do_cleanup: | ||
cleanup() | ||
|
||
if urltemplate: | ||
target['url'] = urltemplate % target | ||
if 'github' in urltemplate: | ||
target['download_url'] = "%s/tarball/master" % target['url'] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. flesh out There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
||
build_setup_cfg_for_bdist_rpm(target) | ||
x = parse_target(target) | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if
setuptools
is not available? It's not a part of the standard Python library...