Merge "Use testr instead of nose."

This commit is contained in:
Jenkins
2013-05-17 21:16:16 +00:00
committed by Gerrit Code Review
10 changed files with 266 additions and 218 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,6 @@
.coverage .coverage
.testrepository
subunit.log
.venv .venv
*,cover *,cover
cover cover

4
.testr.conf Normal file
View File

@@ -0,0 +1,4 @@
[DEFAULT]
test_command=${PYTHON:-python} -m subunit.run discover -t ./ ./tests $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE
test_list_option=--list

14
HACKING
View File

@@ -81,3 +81,17 @@ Exceptions
When dealing with exceptions from underlying libraries, translate those When dealing with exceptions from underlying libraries, translate those
exceptions to an instance or subclass of ClientException. exceptions to an instance or subclass of ClientException.
=======
Testing
=======
python-keystoneclient uses testtools and testr for its unittest suite
and its test runner. Basic workflow around our use of tox and testr can
be found at http://wiki.openstack.org/testr. If you'd like to learn more
in depth:
https://testtools.readthedocs.org/
https://testrepository.readthedocs.org/
Happy hacking!

View File

@@ -1,67 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack LLC
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# Test support for middleware authentication
#
import os
import sys
ROOTDIR = os.path.abspath(os.curdir)
def rootdir(*p):
return os.path.join(ROOTDIR, *p)
class NoModule(object):
"""A mixin class to provide support for unloading/disabling modules."""
def __init__(self, *args, **kw):
super(NoModule, self).__init__(*args, **kw)
self._finders = []
self._cleared_modules = {}
def tearDown(self):
super(NoModule, self).tearDown()
for finder in self._finders:
sys.meta_path.remove(finder)
sys.modules.update(self._cleared_modules)
def clear_module(self, module):
cleared_modules = {}
for fullname in sys.modules.keys():
if fullname == module or fullname.startswith(module + '.'):
cleared_modules[fullname] = sys.modules.pop(fullname)
return cleared_modules
def disable_module(self, module):
"""Ensure ImportError for the specified module."""
# Clear 'module' references in sys.modules
self._cleared_modules.update(self.clear_module(module))
# Disallow further imports of 'module'
class NoModule(object):
def find_module(self, fullname, path):
if fullname == module or fullname.startswith(module + '.'):
raise ImportError
finder = NoModule()
self._finders.append(finder)
sys.meta_path.insert(0, finder)

View File

@@ -14,6 +14,7 @@ function usage {
echo " -p, --pep8 Just run pep8" echo " -p, --pep8 Just run pep8"
echo " -P, --no-pep8 Don't run pep8" echo " -P, --no-pep8 Don't run pep8"
echo " -c, --coverage Generate coverage report" echo " -c, --coverage Generate coverage report"
echo " -d, --debug Run tests with testtools instead of testr. This allows you to use the debugger."
echo " -h, --help Print this usage message" echo " -h, --help Print this usage message"
echo " --hide-elapsed Don't print the elapsed time for each test along with slow test list" echo " --hide-elapsed Don't print the elapsed time for each test along with slow test list"
echo "" echo ""
@@ -33,8 +34,9 @@ function process_option {
-p|--pep8) just_pep8=1;; -p|--pep8) just_pep8=1;;
-P|--no-pep8) no_pep8=1;; -P|--no-pep8) no_pep8=1;;
-c|--coverage) coverage=1;; -c|--coverage) coverage=1;;
-*) noseopts="$noseopts $1";; -d|--debug) debug=1;;
*) noseargs="$noseargs $1" -*) testropts="$testropts $1";;
*) testrargs="$testrargs $1"
esac esac
} }
@@ -45,34 +47,86 @@ never_venv=0
force=0 force=0
no_site_packages=0 no_site_packages=0
installvenvopts= installvenvopts=
noseargs= testrargs=
noseopts= testropts=
wrapper="" wrapper=""
just_pep8=0 just_pep8=0
no_pep8=0 no_pep8=0
coverage=0 coverage=0
debug=0
LANG=en_US.UTF-8
LANGUAGE=en_US:en
LC_ALL=C
OS_STDOUT_NOCAPTURE=False
OS_STDERR_NOCAPTURE=False
for arg in "$@"; do for arg in "$@"; do
process_option $arg process_option $arg
done done
# If enabled, tell nose to collect coverage data
if [ $coverage -eq 1 ]; then
noseopts="$noseopts --with-coverage --cover-package=keystoneclient"
fi
if [ $no_site_packages -eq 1 ]; then if [ $no_site_packages -eq 1 ]; then
installvenvopts="--no-site-packages" installvenvopts="--no-site-packages"
fi fi
function init_testr {
if [ ! -d .testrepository ]; then
${wrapper} testr init
fi
}
function run_tests { function run_tests {
# Cleanup *.pyc
${wrapper} find . -type f -name "*.pyc" -delete
if [ $debug -eq 1 ]; then
if [ "$testropts" = "" ] && [ "$testrargs" = "" ]; then
# Default to running all tests if specific test is not
# provided.
testrargs="discover ./tests"
fi
${wrapper} python -m testtools.run $testropts $testrargs
# Short circuit because all of the testr and coverage stuff
# below does not make sense when running testtools.run for
# debugging purposes.
return $?
fi
if [ $coverage -eq 1 ]; then
TESTRTESTS="$TESTRTESTS --coverage"
else
TESTRTESTS="$TESTRTESTS"
fi
# Just run the test suites in current environment # Just run the test suites in current environment
${wrapper} $NOSETESTS set +e
# If we get some short import error right away, print the error log directly testrargs=`echo "$testrargs" | sed -e's/^\s*\(.*\)\s*$/\1/'`
TESTRTESTS="$TESTRTESTS --testr-args='$testropts $testrargs'"
echo "Running \`${wrapper} $TESTRTESTS\`"
bash -c "${wrapper} $TESTRTESTS"
RESULT=$? RESULT=$?
set -e
copy_subunit_log
if [ $coverage -eq 1 ]; then
echo "Generating coverage report in covhtml/"
# Don't compute coverage for common code, which is tested elsewhere
${wrapper} coverage combine
${wrapper} coverage html --include='keystoneclient/*' --omit='keystoneclient/openstack/common/*' -d covhtml -i
fi
return $RESULT return $RESULT
} }
function copy_subunit_log {
LOGNAME=`cat .testrepository/next-stream`
LOGNAME=$(($LOGNAME - 1))
LOGNAME=".testrepository/${LOGNAME}"
cp $LOGNAME subunit.log
}
function run_pep8 { function run_pep8 {
echo "Running pep8 ..." echo "Running pep8 ..."
srcfiles="keystoneclient tests" srcfiles="keystoneclient tests"
@@ -85,7 +139,7 @@ function run_pep8 {
${srcfiles} ${srcfiles}
} }
NOSETESTS="nosetests $noseopts $noseargs" TESTRTESTS="python setup.py testr"
if [ $never_venv -eq 0 ] if [ $never_venv -eq 0 ]
then then
@@ -123,19 +177,15 @@ if [ $just_pep8 -eq 1 ]; then
exit exit
fi fi
init_testr
run_tests run_tests
# NOTE(sirp): we only want to run pep8 when we're running the full-test suite, # NOTE(sirp): we only want to run pep8 when we're running the full-test suite,
# not when we're running tests individually. To handle this, we need to # not when we're running tests individually. To handle this, we need to
# distinguish between options (noseopts), which begin with a '-', and # distinguish between options (testropts), which begin with a '-', and
# arguments (noseargs). # arguments (testrargs).
if [ -z "$noseargs" ]; then if [ -z "$testrargs" ]; then
if [ $no_pep8 -eq 0 ]; then if [ $no_pep8 -eq 0 ]; then
run_pep8 run_pep8
fi fi
fi fi
if [ $coverage -eq 1 ]; then
echo "Generating coverage report in covhtml/"
${wrapper} coverage html -d covhtml -i
fi

View File

@@ -1,10 +1,3 @@
[nosetests]
verbosity=2
detailed-errors=1
cover-package = keystoneclient
cover-erase = true
cover-inclusive = true
[build_sphinx] [build_sphinx]
source-dir = doc/source source-dir = doc/source
build-dir = doc/build build-dir = doc/build

View File

@@ -37,7 +37,6 @@ setuptools.setup(
cmdclass=setup.get_cmdclass(), cmdclass=setup.get_cmdclass(),
tests_require=tests_require, tests_require=tests_require,
test_suite="nose.collector",
entry_points={ entry_points={
'console_scripts': ['keystone = keystoneclient.shell:main'] 'console_scripts': ['keystone = keystoneclient.shell:main']

View File

@@ -22,6 +22,7 @@ import sys
import tempfile import tempfile
import testtools import testtools
import fixtures
import webob import webob
from keystoneclient.common import cms from keystoneclient.common import cms
@@ -31,12 +32,13 @@ from keystoneclient.middleware import memcache_crypt
from keystoneclient.openstack.common import memorycache from keystoneclient.openstack.common import memorycache
from keystoneclient.openstack.common import jsonutils from keystoneclient.openstack.common import jsonutils
from keystoneclient.openstack.common import timeutils from keystoneclient.openstack.common import timeutils
from keystoneclient.middleware import test
CERTDIR = test.rootdir('examples', 'pki', 'certs') ROOTDIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
KEYDIR = test.rootdir('examples', 'pki', 'private')
CMSDIR = test.rootdir('examples', 'pki', 'cms') CERTDIR = os.path.join(ROOTDIR, "examples/pki/certs")
KEYDIR = os.path.join(ROOTDIR, "examples/pki/private")
CMSDIR = os.path.join(ROOTDIR, "examples/pki/cms")
SIGNING_CERT = os.path.join(CERTDIR, 'signing_cert.pem') SIGNING_CERT = os.path.join(CERTDIR, 'signing_cert.pem')
SIGNING_KEY = os.path.join(KEYDIR, 'signing_key.pem') SIGNING_KEY = os.path.join(KEYDIR, 'signing_key.pem')
CA = os.path.join(CERTDIR, 'ca.pem') CA = os.path.join(CERTDIR, 'ca.pem')
@@ -239,95 +241,96 @@ EXPECTED_V2_DEFAULT_ENV_RESPONSE = {
FAKE_RESPONSE_STACK = [] FAKE_RESPONSE_STACK = []
# @TODO(mordred) This should become a testresources resource attached to the
# class
# The data for these tests are signed using openssl and are stored in files # The data for these tests are signed using openssl and are stored in files
# in the signing subdirectory. In order to keep the values consistent between # in the signing subdirectory. In order to keep the values consistent between
# the tests and the signed documents, we read them in for use in the tests. # the tests and the signed documents, we read them in for use in the tests.
def setUpModule(self): signing_path = CMSDIR
signing_path = CMSDIR with open(os.path.join(signing_path, 'auth_token_scoped.pem')) as f:
with open(os.path.join(signing_path, 'auth_token_scoped.pem')) as f: SIGNED_TOKEN_SCOPED = cms.cms_to_token(f.read())
self.SIGNED_TOKEN_SCOPED = cms.cms_to_token(f.read()) with open(os.path.join(signing_path, 'auth_token_unscoped.pem')) as f:
with open(os.path.join(signing_path, 'auth_token_unscoped.pem')) as f: SIGNED_TOKEN_UNSCOPED = cms.cms_to_token(f.read())
self.SIGNED_TOKEN_UNSCOPED = cms.cms_to_token(f.read()) with open(os.path.join(signing_path, 'auth_v3_token_scoped.pem')) as f:
with open(os.path.join(signing_path, 'auth_v3_token_scoped.pem')) as f: SIGNED_v3_TOKEN_SCOPED = cms.cms_to_token(f.read())
self.SIGNED_v3_TOKEN_SCOPED = cms.cms_to_token(f.read()) with open(os.path.join(signing_path, 'auth_token_revoked.pem')) as f:
with open(os.path.join(signing_path, 'auth_token_revoked.pem')) as f: REVOKED_TOKEN = cms.cms_to_token(f.read())
self.REVOKED_TOKEN = cms.cms_to_token(f.read()) REVOKED_TOKEN_HASH = utils.hash_signed_token(REVOKED_TOKEN)
self.REVOKED_TOKEN_HASH = utils.hash_signed_token(self.REVOKED_TOKEN) with open(os.path.join(signing_path, 'auth_v3_token_revoked.pem')) as f:
with open(os.path.join(signing_path, 'auth_v3_token_revoked.pem')) as f: REVOKED_v3_TOKEN = cms.cms_to_token(f.read())
self.REVOKED_v3_TOKEN = cms.cms_to_token(f.read()) REVOKED_v3_TOKEN_HASH = utils.hash_signed_token(REVOKED_v3_TOKEN)
self.REVOKED_v3_TOKEN_HASH = utils.hash_signed_token(self.REVOKED_v3_TOKEN) with open(os.path.join(signing_path, 'revocation_list.json')) as f:
with open(os.path.join(signing_path, 'revocation_list.json')) as f: REVOCATION_LIST = jsonutils.loads(f.read())
self.REVOCATION_LIST = jsonutils.loads(f.read()) with open(os.path.join(signing_path, 'revocation_list.pem')) as f:
with open(os.path.join(signing_path, 'revocation_list.pem')) as f: VALID_SIGNED_REVOCATION_LIST = jsonutils.dumps(
self.VALID_SIGNED_REVOCATION_LIST = jsonutils.dumps( {'signed': f.read()})
{'signed': f.read()}) SIGNED_TOKEN_SCOPED_KEY =\
self.SIGNED_TOKEN_SCOPED_KEY = ( cms.cms_hash_token(SIGNED_TOKEN_SCOPED)
cms.cms_hash_token(self.SIGNED_TOKEN_SCOPED)) SIGNED_TOKEN_UNSCOPED_KEY =\
self.SIGNED_TOKEN_UNSCOPED_KEY = ( cms.cms_hash_token(SIGNED_TOKEN_UNSCOPED)
cms.cms_hash_token(self.SIGNED_TOKEN_UNSCOPED)) SIGNED_v3_TOKEN_SCOPED_KEY = (
self.SIGNED_v3_TOKEN_SCOPED_KEY = ( cms.cms_hash_token(SIGNED_v3_TOKEN_SCOPED))
cms.cms_hash_token(self.SIGNED_v3_TOKEN_SCOPED))
self.TOKEN_RESPONSES[self.SIGNED_TOKEN_SCOPED_KEY] = { TOKEN_RESPONSES[SIGNED_TOKEN_SCOPED_KEY] = {
'access': { 'access': {
'token': { 'token': {
'id': self.SIGNED_TOKEN_SCOPED_KEY, 'id': SIGNED_TOKEN_SCOPED_KEY,
},
'user': {
'id': 'user_id1',
'name': 'user_name1',
'tenantId': 'tenant_id1',
'tenantName': 'tenant_name1',
'roles': [
{'name': 'role1'},
{'name': 'role2'},
],
},
}, },
} 'user': {
'id': 'user_id1',
self.TOKEN_RESPONSES[SIGNED_TOKEN_UNSCOPED_KEY] = { 'name': 'user_name1',
'access': { 'tenantId': 'tenant_id1',
'token': { 'tenantName': 'tenant_name1',
'id': SIGNED_TOKEN_UNSCOPED_KEY, 'roles': [
}, {'name': 'role1'},
'user': { {'name': 'role2'},
'id': 'user_id1', ],
'name': 'user_name1',
'roles': [
{'name': 'role1'},
{'name': 'role2'},
],
},
}, },
}, },
}
self.TOKEN_RESPONSES[self.SIGNED_v3_TOKEN_SCOPED_KEY] = { TOKEN_RESPONSES[SIGNED_TOKEN_UNSCOPED_KEY] = {
'access': {
'token': { 'token': {
'expires': '2020-01-01T00:00:10.000123Z', 'id': SIGNED_TOKEN_UNSCOPED_KEY,
'user': { },
'id': 'user_id1', 'user': {
'name': 'user_name1', 'id': 'user_id1',
'domain': { 'name': 'user_name1',
'id': 'domain_id1',
'name': 'domain_name1'
}
},
'project': {
'id': 'tenant_id1',
'name': 'tenant_name1',
'domain': {
'id': 'domain_id1',
'name': 'domain_name1'
}
},
'roles': [ 'roles': [
{'name': 'role1'}, {'name': 'role1'},
{'name': 'role2'} {'name': 'role2'},
], ],
'catalog': {} },
} },
}
TOKEN_RESPONSES[SIGNED_v3_TOKEN_SCOPED_KEY] = {
'token': {
'expires': '2020-01-01T00:00:10.000123Z',
'user': {
'id': 'user_id1',
'name': 'user_name1',
'domain': {
'id': 'domain_id1',
'name': 'domain_name1'
}
},
'project': {
'id': 'tenant_id1',
'name': 'tenant_name1',
'domain': {
'id': 'domain_id1',
'name': 'domain_name1'
}
},
'roles': [
{'name': 'role1'},
{'name': 'role2'}
],
'catalog': {}
} }
}
VERSION_LIST_v3 = { VERSION_LIST_v3 = {
"versions": { "versions": {
@@ -362,6 +365,53 @@ VERSION_LIST_v2 = {
} }
class NoModuleFinder(object):
""" Disallow further imports of 'module' """
def __init__(self, module):
self.module = module
def find_module(self, fullname, path):
if fullname == self.module or fullname.startswith(self.module + '.'):
raise ImportError
class DisableModuleFixture(fixtures.Fixture):
"""A fixture to provide support for unloading/disabling modules."""
def __init__(self, module, *args, **kw):
super(DisableModuleFixture, self).__init__(*args, **kw)
self.module = module
self._finders = []
self._cleared_modules = {}
def tearDown(self):
super(DisableModuleFixture, self).tearDown()
for finder in self._finders:
sys.meta_path.remove(finder)
sys.modules.update(self._cleared_modules)
def clear_module(self):
cleared_modules = {}
for fullname in sys.modules.keys():
if (fullname == self.module or
fullname.startswith(self.module + '.')):
cleared_modules[fullname] = sys.modules.pop(fullname)
return cleared_modules
def setUp(self):
"""Ensure ImportError for the specified module."""
super(DisableModuleFixture, self).setUp()
# Clear 'module' references in sys.modules
self._cleared_modules.update(self.clear_module())
finder = NoModuleFinder(self.module)
self._finders.append(finder)
sys.meta_path.insert(0, finder)
class FakeSwiftMemcacheRing(memorycache.Client): class FakeSwiftMemcacheRing(memorycache.Client):
# NOTE(vish): swift memcache uses param timeout instead of time # NOTE(vish): swift memcache uses param timeout instead of time
def set(self, key, value, timeout=0, min_compress_len=0): def set(self, key, value, timeout=0, min_compress_len=0):
@@ -741,7 +791,36 @@ class DiabloAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest):
self.assertTrue('keystone.token_info' in req.environ) self.assertTrue('keystone.token_info' in req.environ)
class AuthTokenMiddlewareTest(test.NoModule, BaseAuthTokenMiddlewareTest): class NoMemcacheAuthToken(BaseAuthTokenMiddlewareTest):
def setUp(self):
super(NoMemcacheAuthToken, self).setUp()
self.useFixture(DisableModuleFixture('memcache'))
def test_nomemcache(self):
conf = {
'admin_token': 'admin_token1',
'auth_host': 'keystone.example.com',
'auth_port': 1234,
'memcache_servers': 'localhost:11211',
}
auth_token.AuthProtocol(FakeApp(), conf)
def test_not_use_cache_from_env(self):
env = {'swift.cache': 'CACHE_TEST'}
conf = {
'auth_host': 'keystone.example.com',
'auth_port': 1234,
'auth_admin_prefix': '/testadmin',
'memcache_servers': 'localhost:11211'
}
self.set_middleware(conf=conf)
self.middleware._init_cache(env)
self.assertNotEqual(self.middleware._cache, 'CACHE_TEST')
class AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest):
def test_init_does_not_call_http(self): def test_init_does_not_call_http(self):
conf = { conf = {
@@ -975,17 +1054,6 @@ class AuthTokenMiddlewareTest(test.NoModule, BaseAuthTokenMiddlewareTest):
self.middleware._cache_initialized = True self.middleware._cache_initialized = True
self.test_memcache_set_expired() self.test_memcache_set_expired()
def test_nomemcache(self):
self.disable_module('memcache')
conf = {
'auth_host': 'keystone.example.com',
'auth_port': 1234,
'auth_admin_prefix': '/testadmin',
'memcache_servers': 'localhost:11211'
}
self.set_middleware(conf=conf)
def test_use_cache_from_env(self): def test_use_cache_from_env(self):
env = {'swift.cache': 'CACHE_TEST'} env = {'swift.cache': 'CACHE_TEST'}
conf = { conf = {
@@ -999,18 +1067,6 @@ class AuthTokenMiddlewareTest(test.NoModule, BaseAuthTokenMiddlewareTest):
self.middleware._init_cache(env) self.middleware._init_cache(env)
self.assertEqual(self.middleware._cache, 'CACHE_TEST') self.assertEqual(self.middleware._cache, 'CACHE_TEST')
def test_not_use_cache_from_env(self):
env = {'swift.cache': 'CACHE_TEST'}
conf = {
'auth_host': 'keystone.example.com',
'auth_port': 1234,
'auth_admin_prefix': '/testadmin',
'memcache_servers': 'localhost:11211'
}
self.set_middleware(conf=conf)
self.middleware._init_cache(env)
self.assertNotEqual(self.middleware._cache, 'CACHE_TEST')
def test_will_expire_soon(self): def test_will_expire_soon(self):
tenseconds = datetime.datetime.utcnow() + datetime.timedelta( tenseconds = datetime.datetime.utcnow() + datetime.timedelta(
seconds=10) seconds=10)
@@ -1176,7 +1232,7 @@ class AuthTokenMiddlewareTest(test.NoModule, BaseAuthTokenMiddlewareTest):
datetime.timedelta(seconds=24)) datetime.timedelta(seconds=24))
class v2AuthTokenMiddlewareTest(test.NoModule, BaseAuthTokenMiddlewareTest): class v2AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest):
""" v2 token specific tests. """ v2 token specific tests.
There are some differences between how the auth-token middleware handles There are some differences between how the auth-token middleware handles

View File

@@ -5,13 +5,10 @@ fixtures
keyring keyring
mock mock
mox mox
nose
nose-exclude
openstack.nose_plugin
nosehtmloutput
pep8==1.3.3 pep8==1.3.3
pycrypto pycrypto
sphinx>=1.1.2 sphinx>=1.1.2
testrepository>=0.0.13
testtools>=0.9.22 testtools>=0.9.22
WebOb>=1.0.8 WebOb>=1.0.8

26
tox.ini
View File

@@ -3,24 +3,24 @@ envlist = py26,py27,pep8
[testenv] [testenv]
setenv = VIRTUAL_ENV={envdir} setenv = VIRTUAL_ENV={envdir}
NOSE_WITH_OPENSTACK=1 LANG=en_US.UTF-8
NOSE_OPENSTACK_COLOR=1 LANGUAGE=en_US:en
NOSE_OPENSTACK_RED=0.05 LC_ALL=C
NOSE_OPENSTACK_YELLOW=0.025 OS_STDOUT_NOCAPTURE=False
NOSE_OPENSTACK_SHOW_ELAPSED=1 OS_STDERR_NOCAPTURE=False
NOSE_OPENSTACK_STDOUT=1
deps = -r{toxinidir}/tools/pip-requires deps = -r{toxinidir}/tools/pip-requires
-r{toxinidir}/tools/test-requires -r{toxinidir}/tools/test-requires
commands = nosetests {posargs} commands = python setup.py testr --testr-args='{posargs}'
[tox:jenkins]
downloadcache = ~/cache/pip
[testenv:pep8] [testenv:pep8]
commands = pep8 --repeat --show-source --ignore=E711,E712,E125,E126 --exclude=.venv,.tox,dist,doc . commands = pep8 --repeat --show-source --ignore=E711,E712,E125,E126 --exclude=.venv,.tox,dist,doc .
[testenv:cover]
setenv = NOSE_WITH_COVERAGE=1
[testenv:venv] [testenv:venv]
commands = {posargs} commands = {posargs}
[testenv:cover]
commands = python setup.py testr --coverage --testr-args='{posargs}'
[tox:jenkins]
downloadcache = ~/cache/pip