Skip to content
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

Merged
merged 8 commits into from
Oct 9, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
311 changes: 236 additions & 75 deletions lib/vsc/install/shared_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

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...

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')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe keep them ordered alphabetically?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, are you using a different alphabet than the rest of us? ;)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you? (and are you suggestion emacs' sort-fields is wrong 😄 )

Copy link
Member

Choose a reason for hiding this comment

The 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))$')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do I dare ask about this?

Copy link
Member Author

Choose a reason for hiding this comment

The 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

Expand All @@ -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."""
Expand Down Expand Up @@ -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')
Copy link
Member

Choose a reason for hiding this comment

The 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing docstring

self.test_filterm = None
self.test_filterf = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what magic takes care of populating these if -f or -F are used?

Copy link
Member Author

Choose a reason for hiding this comment

The 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])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can verify whether it is, by checking os.path.basename(setup_py)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, what if someone uses it from a setup_whatever.py to test branches?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On 09/10/15 09:48, Stijn De Weirdt wrote:

In lib/vsc/install/shared_setup.py
#193 (comment):

  • ]
  • def initialize_options(self):
  •    TestCommand.initialize_options(self)
    
  •    self.test_filterm = None
    
  •    self.test_filterf = None
    
  •    self.test_loader = "vsc.install.shared_setup:VscScanningLoader"
    
  • def run_tests(self):
  •    getattr(**builtin**,'__test_filter').update({
    
  •        'function': self.test_filterf,
    
  •        'module': self.test_filterm,
    
  •    })
    
  •    # should be setup.py
    
  •    setup_py = os.path.abspath(sys.argv[0])
    

hmm, what if someone uses it from a |setup_whatever.py| to test branches?

Then the check will fail. You can make it a bit more flexible if you
need to:

os.path.basename(setup_py).startswith('setup')
os.path.basename(setup_py).endswith('.py')

log.info('run_tests from %s' % setup_py)
Copy link
Contributor

Choose a reason for hiding this comment

The 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)
why not use sys.argv[0]? Seems to do the same thing to me in these cases:

[jens@exile temp]$ cat foo.py 
import bla
[jens@exile temp]$ cat bla.py
import inspect
import sys
import os
print inspect.stack()[-1][1]
print sys.argv[0] 
print os.path.dirname(sys.argv[0])
print os.path.dirname(inspect.stack()[-1][1])
[jens@exile temp]$ python foo.py 
foo.py
foo.py


[jens@exile temp]$ python bla.py
bla.py
bla.py


[jens@exile temp]$ mv foo.py test/
[jens@exile temp]$ vim bla.py
[jens@exile temp]$ mv bla.py test/
[jens@exile temp]$ python test/bla.py 
test/bla.py
test/bla.py
test
test
[jens@exile temp]$ python test/foo.py 
test/foo.py
test/foo.py
test
test

Copy link
Member Author

Choose a reason for hiding this comment

The 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 sys.argv[0]

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no cleanup?

Copy link
Member Author

Choose a reason for hiding this comment

The 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.
but i'll add the cleanup for good practise.

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait, shouldn't this add base_dir rather than test_dir to sys.path?

Copy link
Member Author

Choose a reason for hiding this comment

The 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

Copy link
Member

Choose a reason for hiding this comment

The 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': '',
Expand All @@ -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,
}


Expand Down Expand Up @@ -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
Expand All @@ -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']
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flesh out master into a branch named argument?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


build_setup_cfg_for_bdist_rpm(target)
x = parse_target(target)

Expand Down
Loading