Changeset - d88077fae3d6
[Not reviewed]
default
0 3 1
Thomas De Schampheleire - 10 years ago 2016-02-03 22:32:28
thomas.de.schampheleire@gmail.com
pytest migration: switch to pytest; remove nose support

Make pytest the default test runner and remove support for nose.
Tests can be run using:
- py.test
- python setup.py test

The pytest configuration needs to move from setup.cfg to pytest.ini to support
this - see https://github.com/pytest-dev/pytest/issues/567 and
https://bitbucket.org/pytest-dev/pytest-runner/issues/7/support-all-pytest-commands .
4 files changed with 15 insertions and 28 deletions:
0 comments (0 inline, 0 general)
kallithea/tests/parameterized.py
Show inline comments
 
import re
 
import new
 
import inspect
 
import logging
 
import logging.handlers
 
from functools import wraps
 

	
 
from unittest import TestCase
 

	
 

	
 
def skip_test(func):
 
    try:
 
        from nose.tools import nottest
 
    except ImportError:
 
        pass
 
    else:
 
        func = nottest(func)
 

	
 
    try:
 
        import pytest
 
    except ImportError:
 
        pass
 
    else:
 
        func = pytest.mark.skipIf(True, func)
 

	
 
    return func
 

	
 

	
 
def _terrible_magic_get_defining_classes():
 
    """ Returns the set of parent classes of the class currently being defined.
 
        Will likely only work if called from the ``parameterized`` decorator.
 
        This function is entirely @brandon_rhodes's fault, as he suggested
 
        the implementation: http://stackoverflow.com/a/8793684/71522
 
        """
 
    stack = inspect.stack()
 
    if len(stack) <= 4:
 
        return []
 
    frame = stack[3]
 
    code_context = frame[4][0].strip()
 
    if not code_context.startswith("class "):
 
        return []
 
    _, parents = code_context.split("(", 1)
 
    parents, _ = parents.rsplit(")", 1)
 
    return eval("[" + parents + "]", frame[0].f_globals, frame[0].f_locals)
 

	
 

	
 
def parameterized(input):
 
    """ Parameterize a test case:
 
        >>> add1_tests = [(1, 2), (2, 3)]
 
        >>> class TestFoo(object):
 
        ...     @parameterized(add1_tests)
 
        ...     def test_add1(self, input, expected):
 
        ...         assert_equal(add1(input), expected)
 
        >>> @parameterized(add1_tests)
 
        ... def test_add1(input, expected):
 
        ...     assert_equal(add1(input), expected)
 
        >>>
 
        """
 

	
 
    if not hasattr(input, "__iter__"):
 
        raise ValueError("expected iterable input; got %r" % (input,))
 

	
 
    def parameterized_helper(f):
 
        attached_instance_method = [False]
 

	
 
        parent_classes = _terrible_magic_get_defining_classes()
 
        if any(issubclass(cls, TestCase) for cls in parent_classes):
 
            raise Exception("Warning: '@parameterized' tests won't work "
 
                            "inside subclasses of 'TestCase' - use "
 
                            "'@parameterized.expand' instead")
 

	
 
        @wraps(f)
 
        def parameterized_helper_method(self=None):
 
            if self is not None and not attached_instance_method[0]:
 
                # confusingly, we need to create a named instance method and
 
                # attach that to the class...
 
                cls = self.__class__
 
                im_f = new.instancemethod(f, None, cls)
 
                setattr(cls, f.__name__, im_f)
 
                attached_instance_method[0] = True
 
            for args in input:
 
                if isinstance(args, basestring):
 
                    args = [args]
 
                # ... then pull that named instance method off, turning it into
 
                # a bound method ...
 
                if self is not None:
 
                    args = [getattr(self, f.__name__)] + list(args)
 
                else:
 
                    args = [f] + list(args)
 
                # ... then yield that as a tuple. If those steps aren't
 
                # followed precicely, Nose gets upset and doesn't run the test
 
                # or doesn't run setup methods.
 
                yield tuple(args)
 

	
 
        f.__name__ = "_helper_for_%s" % (f.__name__,)
 
        parameterized_helper_method.parameterized_input = input
 
        parameterized_helper_method.parameterized_func = f
 
        return parameterized_helper_method
 

	
 
    return parameterized_helper
 

	
 

	
 
def to_safe_name(s):
 
    return re.sub("[^a-zA-Z0-9_]", "", s)
 

	
 

	
 
def parameterized_expand_helper(func_name, func, args):
 
    def parameterized_expand_helper_helper(self=()):
 
        if self != ():
 
            self = (self,)
 
        return func(*(self + args))
 
    parameterized_expand_helper_helper.__name__ = str(func_name)
 
    return parameterized_expand_helper_helper
 

	
 

	
 
def parameterized_expand(input):
 
    """ A "brute force" method of parameterizing test cases. Creates new test
 
        cases and injects them into the namespace that the wrapped function
 
        is being defined in. Useful for parameterizing tests in subclasses
 
        of 'UnitTest', where Nose test generators don't work.
 

	
 
        >>> @parameterized.expand([("foo", 1, 2)])
 
        ... def test_add1(name, input, expected):
 
        ...     actual = add1(input)
 
        ...     assert_equal(actual, expected)
 
        ...
 
        >>> locals()
 
        ... 'test_add1_foo_0': <function ...> ...
 
        >>>
 
        """
 

	
 
    def parameterized_expand_wrapper(f):
 
        stack = inspect.stack()
 
        frame = stack[1]
 
        frame_locals = frame[0].f_locals
 

	
 
        base_name = f.__name__
 
        for num, args in enumerate(input):
 
            name_suffix = "_%s" % (num,)
 
            if len(args) > 0 and isinstance(args[0], basestring):
 
                name_suffix += "_" + to_safe_name(args[0])
 
            name = base_name + name_suffix
 
            new_func = parameterized_expand_helper(name, f, args)
 
            frame_locals[name] = new_func
 
        return skip_test(f)
 
    return parameterized_expand_wrapper
 

	
 
parameterized.expand = parameterized_expand
 

	
 

	
 
def assert_contains(haystack, needle):
 
    if needle not in haystack:
 
        raise AssertionError("%r not in %r" % (needle, haystack))
 

	
 

	
 
def assert_not_contains(haystack, needle):
 
    if needle in haystack:
 
        raise AssertionError("%r in %r" % (needle, haystack))
 

	
 

	
 
def assert_raises(func, exc_type, str_contains=None, repr_contains=None):
 
    try:
 
        func()
 
    except exc_type as e:
 
        if str_contains is not None and str_contains not in str(e):
 
            raise AssertionError("%s raised, but %r does not contain %r"
 
                                 % (exc_type, str(e), str_contains))
 
        if repr_contains is not None and repr_contains not in repr(e):
 
            raise AssertionError("%s raised, but %r does not contain %r"
 
                                 % (exc_type, repr(e), repr_contains))
 
        return e
 
    else:
 
        raise AssertionError("%s not raised" % (exc_type,))
 

	
 

	
 
log_handler = None
 

	
 

	
 
def setup_logging():
 
    """ Configures a log handler which will capure log messages during a test.
 
        The ``logged_messages`` and ``assert_no_errors_logged`` functions can be
 
        used to make assertions about these logged messages.
 

	
 
        For example::
 

	
 
            from ensi_common.testing import (
 
                setup_logging, teardown_logging, assert_no_errors_logged,
 
                assert_logged,
 
            )
 

	
 
            class TestWidget(object):
 
                def setup(self):
 
                    setup_logging()
 

	
 
                def teardown(self):
 
                    assert_no_errors_logged()
 
                    teardown_logging()
 

	
 
                def test_that_will_fail(self):
 
                    log.warning("this warning message will trigger a failure")
 

	
 
                def test_that_will_pass(self):
 
                    log.info("but info messages are ok")
 
                    assert_logged("info messages are ok")
 
        """
 

	
 
    global log_handler
 
    if log_handler is not None:
 
        logging.getLogger().removeHandler(log_handler)
 
    log_handler = logging.handlers.BufferingHandler(1000)
 
    formatter = logging.Formatter("%(name)s: %(levelname)s: %(message)s")
 
    log_handler.setFormatter(formatter)
 
    logging.getLogger().addHandler(log_handler)
 

	
 

	
 
def teardown_logging():
 
    global log_handler
 
    if log_handler is not None:
 
        logging.getLogger().removeHandler(log_handler)
 
        log_handler = None
 

	
 

	
 
def logged_messages():
 
    assert log_handler, "setup_logging not called"
 
    return [(log_handler.format(record), record) for record in log_handler.buffer]
 

	
 

	
 
def assert_no_errors_logged():
 
    for _, record in logged_messages():
 
        if record.levelno >= logging.WARNING:
 
            # Assume that the nose log capture plugin is being used, so it will
 
            # show the exception.
 
            raise AssertionError("an unexpected error was logged")
 

	
 

	
 
def assert_logged(expected_msg_contents):
 
    for msg, _ in logged_messages():
 
        if expected_msg_contents in msg:
 
            return
 
    raise AssertionError("no logged message contains %r"
 
                         % (expected_msg_contents,))
pytest.ini
Show inline comments
 
new file 100644
 
[pytest]
 
# only look for tests in kallithea/tests
 
python_files = kallithea/tests/**/test_*.py
 
addopts =
 
    # --verbose
 
    # show extra test summary info as specified by chars (f)ailed, (E)error, (s)skipped, (x)failed, (X)passed, (w)warnings.
 
    -rfEsxXw
 
    # Shorter scrollbacks; less stuff to scroll through
 
    --tb=short
setup.cfg
Show inline comments
 
[egg_info]
 
tag_build =
 
tag_svn_revision = 0
 
tag_date = 0
 

	
 
[nosetests]
 
verbose = True
 
verbosity = 2
 
with-pylons = kallithea/tests/test.ini
 
detailed-errors = 1
 
nologcapture = 1
 

	
 
[pytest]
 
# only look for tests in kallithea/tests
 
python_files = kallithea/tests/**/test_*.py
 
addopts =
 
    # --verbose
 
    # show extra test summary info as specified by chars (f)ailed, (E)error, (s)skipped, (x)failed, (X)passed, (w)warnings.
 
    -rfEsxXw
 
    # Shorter scrollbacks; less stuff to scroll through
 
    --tb=short
 
[aliases]
 
test = pytest
 

	
 
[compile_catalog]
 
domain = kallithea
 
directory = kallithea/i18n
 
statistics = true
 

	
 
[extract_messages]
 
add_comments = TRANSLATORS:
 
output_file = kallithea/i18n/kallithea.pot
 
msgid-bugs-address = translations@kallithea-scm.org
 
copyright-holder = Various authors, licensing as GPLv3
 
no-wrap = true
 

	
 
[init_catalog]
 
domain = kallithea
 
input_file = kallithea/i18n/kallithea.pot
 
output_dir = kallithea/i18n
 

	
 
[update_catalog]
 
domain = kallithea
 
input_file = kallithea/i18n/kallithea.pot
 
output_dir = kallithea/i18n
 
previous = true
 

	
 
[build_sphinx]
 
source-dir = docs/
 
build-dir = docs/_build
 
all_files = 1
 

	
 
[upload_sphinx]
 
upload-dir = docs/_build/html
setup.py
Show inline comments
 
#!/usr/bin/env python2
 
# -*- coding: utf-8 -*-
 
import os
 
import sys
 
import platform
 

	
 
if sys.version_info < (2, 6):
 
    raise Exception('Kallithea requires python 2.6 or 2.7')
 

	
 

	
 
here = os.path.abspath(os.path.dirname(__file__))
 

	
 

	
 
def _get_meta_var(name, data, callback_handler=None):
 
    import re
 
    matches = re.compile(r'(?:%s)\s*=\s*(.*)' % name).search(data)
 
    if matches:
 
        if not callable(callback_handler):
 
            callback_handler = lambda v: v
 

	
 
        return callback_handler(eval(matches.groups()[0]))
 

	
 
_meta = open(os.path.join(here, 'kallithea', '__init__.py'), 'rb')
 
_metadata = _meta.read()
 
_meta.close()
 

	
 
callback = lambda V: ('.'.join(map(str, V[:3])) + '.'.join(V[3:]))
 
__version__ = _get_meta_var('VERSION', _metadata, callback)
 
__license__ = _get_meta_var('__license__', _metadata)
 
__author__ = _get_meta_var('__author__', _metadata)
 
__url__ = _get_meta_var('__url__', _metadata)
 
# defines current platform
 
__platform__ = platform.system()
 

	
 
is_windows = __platform__ in ['Windows']
 

	
 
requirements = [
 
    "waitress==0.8.8",
 
    "webob>=1.0.8,<=1.1.1",
 
    "webtest==1.4.3",
 
    "Pylons>=1.0.0,<=1.0.2",
 
    "Beaker==1.6.4",
 
    "WebHelpers==1.3",
 
    "formencode>=1.2.4,<=1.2.6",
 
    "SQLAlchemy==0.7.10",
 
    "Mako>=0.9.0,<=1.0.0",
 
    "pygments>=1.5",
 
    "whoosh>=2.4.0,<=2.5.7",
 
    "celery>=2.2.5,<2.3",
 
    "babel>=0.9.6,<=1.3",
 
    "python-dateutil>=1.5.0,<2.0.0",
 
    "markdown==2.2.1",
 
    "docutils>=0.8.1,<=0.11",
 
    "mock",
 
    "URLObject==2.3.4",
 
    "Routes==1.13",
 
    "pytest>=2.7.0,<3.0",
 
    "dulwich>=0.9.9,<=0.9.9",
 
    "mercurial>=2.9,<3.7",
 
]
 

	
 
if sys.version_info < (2, 7):
 
    requirements.append("importlib==1.0.1")
 
    requirements.append("unittest2")
 
    requirements.append("argparse")
 

	
 
if not is_windows:
 
    requirements.append("py-bcrypt>=0.3.0,<=0.4")
 

	
 

	
 
dependency_links = [
 
]
 

	
 
classifiers = [
 
    'Development Status :: 4 - Beta',
 
    'Environment :: Web Environment',
 
    'Framework :: Pylons',
 
    'Intended Audience :: Developers',
 
    'License :: OSI Approved :: GNU General Public License (GPL)',
 
    'Operating System :: OS Independent',
 
    'Programming Language :: Python',
 
    'Programming Language :: Python :: 2.6',
 
    'Programming Language :: Python :: 2.7',
 
    'Topic :: Software Development :: Version Control',
 
]
 

	
 

	
 
# additional files from project that goes somewhere in the filesystem
 
# relative to sys.prefix
 
data_files = []
 

	
 
# additional files that goes into package itself
 
package_data = {'kallithea': ['i18n/*/LC_MESSAGES/*.mo', ], }
 

	
 
description = ('Kallithea is a fast and powerful management tool '
 
               'for Mercurial and Git with a built in push/pull server, '
 
               'full text search and code-review.')
 

	
 
keywords = ' '.join([
 
    'kallithea', 'mercurial', 'git', 'code review',
 
    'repo groups', 'ldap', 'repository management', 'hgweb replacement',
 
    'hgwebdir', 'gitweb replacement', 'serving hgweb',
 
])
 

	
 
# long description
 
README_FILE = 'README.rst'
 
CHANGELOG_FILE = 'docs/changelog.rst'
 
try:
 
    long_description = open(README_FILE).read() + '\n\n' + \
 
        open(CHANGELOG_FILE).read()
 

	
 
except IOError as err:
 
    sys.stderr.write(
 
        "[WARNING] Cannot find file specified as long_description (%s)\n or "
 
        "changelog (%s) skipping that file" % (README_FILE, CHANGELOG_FILE)
 
    )
 
    long_description = description
 

	
 
try:
 
    from setuptools import setup, find_packages
 
except ImportError:
 
    from ez_setup import use_setuptools
 
    use_setuptools()
 
    from setuptools import setup, find_packages
 

	
 
# monkey patch setuptools to use distutils owner/group functionality
 
from setuptools.command import sdist
 
sdist_org = sdist.sdist
 
class sdist_new(sdist_org):
 
    def initialize_options(self):
 
        sdist_org.initialize_options(self)
 
        self.owner = self.group = 'root'
 
sdist.sdist = sdist_new
 

	
 
# packages
 
packages = find_packages(exclude=['ez_setup'])
 

	
 
setup(
 
    name='Kallithea',
 
    version=__version__,
 
    description=description,
 
    long_description=long_description,
 
    keywords=keywords,
 
    license=__license__,
 
    author=__author__,
 
    author_email='kallithea@sfconservancy.org',
 
    dependency_links=dependency_links,
 
    url=__url__,
 
    install_requires=requirements,
 
    classifiers=classifiers,
 
    setup_requires=["PasteScript>=1.6.3"],
 
    setup_requires=['PasteScript>=1.6.3',
 
                    'pytest-runner'],
 
    tests_require=['pytest'],
 
    data_files=data_files,
 
    packages=packages,
 
    include_package_data=True,
 
    test_suite='nose.collector',
 
    package_data=package_data,
 
    message_extractors={'kallithea': [
 
            ('**.py', 'python', None),
 
            ('templates/**.mako', 'mako', {'input_encoding': 'utf-8'}),
 
            ('templates/**.html', 'mako', {'input_encoding': 'utf-8'}),
 
            ('public/**', 'ignore', None)]},
 
    zip_safe=False,
 
    paster_plugins=['PasteScript', 'Pylons'],
 
    entry_points="""
 
    [console_scripts]
 
    kallithea-api =    kallithea.bin.kallithea_api:main
 
    kallithea-gist =   kallithea.bin.kallithea_gist:main
 
    kallithea-config = kallithea.bin.kallithea_config:main
 

	
 
    [paste.app_factory]
 
    main = kallithea.config.middleware:make_app
 

	
 
    [paste.app_install]
 
    main = pylons.util:PylonsInstaller
 

	
 
    [paste.global_paster_command]
 
    setup-db=kallithea.lib.paster_commands.setup_db:Command
 
    cleanup-repos=kallithea.lib.paster_commands.cleanup:Command
 
    update-repoinfo=kallithea.lib.paster_commands.update_repoinfo:Command
 
    make-rcext=kallithea.lib.paster_commands.make_rcextensions:Command
 
    repo-scan=kallithea.lib.paster_commands.repo_scan:Command
 
    cache-keys=kallithea.lib.paster_commands.cache_keys:Command
 
    ishell=kallithea.lib.paster_commands.ishell:Command
 
    make-index=kallithea.lib.paster_commands.make_index:Command
 
    upgrade-db=kallithea.lib.dbmigrate:UpgradeDb
 
    celeryd=kallithea.lib.celerypylons.commands:CeleryDaemonCommand
 
    install-iis=kallithea.lib.paster_commands.install_iis:Command
 

	
 
    [nose.plugins]
 
    pylons = pylons.test:PylonsPlugin
 
    """,
 
)
0 comments (0 inline, 0 general)