diff --git a/.gitignore b/.gitignore index a74a7b333..e51d7492d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .coverage +.testrepository +subunit.log .venv *,cover cover diff --git a/.testr.conf b/.testr.conf new file mode 100644 index 000000000..081907d59 --- /dev/null +++ b/.testr.conf @@ -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 diff --git a/HACKING b/HACKING index bac532fe3..010be5b73 100644 --- a/HACKING +++ b/HACKING @@ -81,3 +81,17 @@ Exceptions When dealing with exceptions from underlying libraries, translate those 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! diff --git a/keystoneclient/middleware/test.py b/keystoneclient/middleware/test.py deleted file mode 100644 index 77511412c..000000000 --- a/keystoneclient/middleware/test.py +++ /dev/null @@ -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) diff --git a/run_tests.sh b/run_tests.sh index 782c36561..12ed568e1 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -14,6 +14,7 @@ function usage { echo " -p, --pep8 Just run pep8" echo " -P, --no-pep8 Don't run pep8" 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 " --hide-elapsed Don't print the elapsed time for each test along with slow test list" echo "" @@ -33,8 +34,9 @@ function process_option { -p|--pep8) just_pep8=1;; -P|--no-pep8) no_pep8=1;; -c|--coverage) coverage=1;; - -*) noseopts="$noseopts $1";; - *) noseargs="$noseargs $1" + -d|--debug) debug=1;; + -*) testropts="$testropts $1";; + *) testrargs="$testrargs $1" esac } @@ -45,34 +47,86 @@ never_venv=0 force=0 no_site_packages=0 installvenvopts= -noseargs= -noseopts= +testrargs= +testropts= wrapper="" just_pep8=0 no_pep8=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 process_option $arg 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 installvenvopts="--no-site-packages" fi +function init_testr { + if [ ! -d .testrepository ]; then + ${wrapper} testr init + fi +} + 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 - ${wrapper} $NOSETESTS - # If we get some short import error right away, print the error log directly + set +e + testrargs=`echo "$testrargs" | sed -e's/^\s*\(.*\)\s*$/\1/'` + TESTRTESTS="$TESTRTESTS --testr-args='$testropts $testrargs'" + echo "Running \`${wrapper} $TESTRTESTS\`" + bash -c "${wrapper} $TESTRTESTS" 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 } +function copy_subunit_log { + LOGNAME=`cat .testrepository/next-stream` + LOGNAME=$(($LOGNAME - 1)) + LOGNAME=".testrepository/${LOGNAME}" + cp $LOGNAME subunit.log +} + function run_pep8 { echo "Running pep8 ..." srcfiles="keystoneclient tests" @@ -85,7 +139,7 @@ function run_pep8 { ${srcfiles} } -NOSETESTS="nosetests $noseopts $noseargs" +TESTRTESTS="python setup.py testr" if [ $never_venv -eq 0 ] then @@ -123,19 +177,15 @@ if [ $just_pep8 -eq 1 ]; then exit fi +init_testr run_tests # 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 -# distinguish between options (noseopts), which begin with a '-', and -# arguments (noseargs). -if [ -z "$noseargs" ]; then +# distinguish between options (testropts), which begin with a '-', and +# arguments (testrargs). +if [ -z "$testrargs" ]; then if [ $no_pep8 -eq 0 ]; then run_pep8 fi fi - -if [ $coverage -eq 1 ]; then - echo "Generating coverage report in covhtml/" - ${wrapper} coverage html -d covhtml -i -fi diff --git a/setup.cfg b/setup.cfg index 342110126..876bc6d54 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,10 +1,3 @@ -[nosetests] -verbosity=2 -detailed-errors=1 -cover-package = keystoneclient -cover-erase = true -cover-inclusive = true - [build_sphinx] source-dir = doc/source build-dir = doc/build diff --git a/setup.py b/setup.py index c6a9e6be9..8bec3bc7e 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,6 @@ setuptools.setup( cmdclass=setup.get_cmdclass(), tests_require=tests_require, - test_suite="nose.collector", entry_points={ 'console_scripts': ['keystone = keystoneclient.shell:main'] diff --git a/tests/test_auth_token_middleware.py b/tests/test_auth_token_middleware.py index 86d9ee25c..5723e156a 100644 --- a/tests/test_auth_token_middleware.py +++ b/tests/test_auth_token_middleware.py @@ -22,6 +22,7 @@ import sys import tempfile import testtools +import fixtures import webob 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 jsonutils from keystoneclient.openstack.common import timeutils -from keystoneclient.middleware import test -CERTDIR = test.rootdir('examples', 'pki', 'certs') -KEYDIR = test.rootdir('examples', 'pki', 'private') -CMSDIR = test.rootdir('examples', 'pki', 'cms') +ROOTDIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + +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_KEY = os.path.join(KEYDIR, 'signing_key.pem') CA = os.path.join(CERTDIR, 'ca.pem') @@ -239,95 +241,96 @@ EXPECTED_V2_DEFAULT_ENV_RESPONSE = { 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 # 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. -def setUpModule(self): - signing_path = CMSDIR - with open(os.path.join(signing_path, 'auth_token_scoped.pem')) as f: - self.SIGNED_TOKEN_SCOPED = cms.cms_to_token(f.read()) - with open(os.path.join(signing_path, 'auth_token_unscoped.pem')) as f: - self.SIGNED_TOKEN_UNSCOPED = cms.cms_to_token(f.read()) - with open(os.path.join(signing_path, 'auth_v3_token_scoped.pem')) as f: - self.SIGNED_v3_TOKEN_SCOPED = cms.cms_to_token(f.read()) - with open(os.path.join(signing_path, 'auth_token_revoked.pem')) as f: - self.REVOKED_TOKEN = cms.cms_to_token(f.read()) - 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: - self.REVOKED_v3_TOKEN = cms.cms_to_token(f.read()) - 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: - self.REVOCATION_LIST = jsonutils.loads(f.read()) - with open(os.path.join(signing_path, 'revocation_list.pem')) as f: - self.VALID_SIGNED_REVOCATION_LIST = jsonutils.dumps( - {'signed': f.read()}) - self.SIGNED_TOKEN_SCOPED_KEY = ( - cms.cms_hash_token(self.SIGNED_TOKEN_SCOPED)) - self.SIGNED_TOKEN_UNSCOPED_KEY = ( - cms.cms_hash_token(self.SIGNED_TOKEN_UNSCOPED)) - self.SIGNED_v3_TOKEN_SCOPED_KEY = ( - cms.cms_hash_token(self.SIGNED_v3_TOKEN_SCOPED)) +signing_path = CMSDIR +with open(os.path.join(signing_path, 'auth_token_scoped.pem')) as f: + SIGNED_TOKEN_SCOPED = cms.cms_to_token(f.read()) +with open(os.path.join(signing_path, 'auth_token_unscoped.pem')) as f: + SIGNED_TOKEN_UNSCOPED = cms.cms_to_token(f.read()) +with open(os.path.join(signing_path, 'auth_v3_token_scoped.pem')) as f: + SIGNED_v3_TOKEN_SCOPED = cms.cms_to_token(f.read()) +with open(os.path.join(signing_path, 'auth_token_revoked.pem')) as f: + REVOKED_TOKEN = cms.cms_to_token(f.read()) +REVOKED_TOKEN_HASH = utils.hash_signed_token(REVOKED_TOKEN) +with open(os.path.join(signing_path, 'auth_v3_token_revoked.pem')) as f: + REVOKED_v3_TOKEN = cms.cms_to_token(f.read()) +REVOKED_v3_TOKEN_HASH = utils.hash_signed_token(REVOKED_v3_TOKEN) +with open(os.path.join(signing_path, 'revocation_list.json')) as f: + REVOCATION_LIST = jsonutils.loads(f.read()) +with open(os.path.join(signing_path, 'revocation_list.pem')) as f: + VALID_SIGNED_REVOCATION_LIST = jsonutils.dumps( + {'signed': f.read()}) +SIGNED_TOKEN_SCOPED_KEY =\ + cms.cms_hash_token(SIGNED_TOKEN_SCOPED) +SIGNED_TOKEN_UNSCOPED_KEY =\ + cms.cms_hash_token(SIGNED_TOKEN_UNSCOPED) +SIGNED_v3_TOKEN_SCOPED_KEY = ( + cms.cms_hash_token(SIGNED_v3_TOKEN_SCOPED)) - self.TOKEN_RESPONSES[self.SIGNED_TOKEN_SCOPED_KEY] = { - 'access': { - 'token': { - 'id': self.SIGNED_TOKEN_SCOPED_KEY, - }, - 'user': { - 'id': 'user_id1', - 'name': 'user_name1', - 'tenantId': 'tenant_id1', - 'tenantName': 'tenant_name1', - 'roles': [ - {'name': 'role1'}, - {'name': 'role2'}, - ], - }, +TOKEN_RESPONSES[SIGNED_TOKEN_SCOPED_KEY] = { + 'access': { + 'token': { + 'id': SIGNED_TOKEN_SCOPED_KEY, }, - } - - self.TOKEN_RESPONSES[SIGNED_TOKEN_UNSCOPED_KEY] = { - 'access': { - 'token': { - 'id': SIGNED_TOKEN_UNSCOPED_KEY, - }, - 'user': { - 'id': 'user_id1', - 'name': 'user_name1', - 'roles': [ - {'name': 'role1'}, - {'name': 'role2'}, - ], - }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', + 'tenantId': 'tenant_id1', + 'tenantName': 'tenant_name1', + 'roles': [ + {'name': 'role1'}, + {'name': 'role2'}, + ], }, }, +} - self.TOKEN_RESPONSES[self.SIGNED_v3_TOKEN_SCOPED_KEY] = { +TOKEN_RESPONSES[SIGNED_TOKEN_UNSCOPED_KEY] = { + 'access': { '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' - } - }, + 'id': SIGNED_TOKEN_UNSCOPED_KEY, + }, + 'user': { + 'id': 'user_id1', + 'name': 'user_name1', 'roles': [ - {'name': 'role1'}, - {'name': 'role2'} + {'name': 'role1'}, + {'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 = { "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): # NOTE(vish): swift memcache uses param timeout instead of time 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) -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): conf = { @@ -975,17 +1054,6 @@ class AuthTokenMiddlewareTest(test.NoModule, BaseAuthTokenMiddlewareTest): self.middleware._cache_initialized = True 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): env = {'swift.cache': 'CACHE_TEST'} conf = { @@ -999,18 +1067,6 @@ class AuthTokenMiddlewareTest(test.NoModule, BaseAuthTokenMiddlewareTest): self.middleware._init_cache(env) 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): tenseconds = datetime.datetime.utcnow() + datetime.timedelta( seconds=10) @@ -1176,7 +1232,7 @@ class AuthTokenMiddlewareTest(test.NoModule, BaseAuthTokenMiddlewareTest): datetime.timedelta(seconds=24)) -class v2AuthTokenMiddlewareTest(test.NoModule, BaseAuthTokenMiddlewareTest): +class v2AuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest): """ v2 token specific tests. There are some differences between how the auth-token middleware handles diff --git a/tools/test-requires b/tools/test-requires index d0abb3298..41348f1b3 100644 --- a/tools/test-requires +++ b/tools/test-requires @@ -5,13 +5,10 @@ fixtures keyring mock mox -nose -nose-exclude -openstack.nose_plugin -nosehtmloutput pep8==1.3.3 pycrypto sphinx>=1.1.2 +testrepository>=0.0.13 testtools>=0.9.22 WebOb>=1.0.8 diff --git a/tox.ini b/tox.ini index 6533a18de..915cab220 100644 --- a/tox.ini +++ b/tox.ini @@ -3,24 +3,24 @@ envlist = py26,py27,pep8 [testenv] setenv = VIRTUAL_ENV={envdir} - NOSE_WITH_OPENSTACK=1 - NOSE_OPENSTACK_COLOR=1 - NOSE_OPENSTACK_RED=0.05 - NOSE_OPENSTACK_YELLOW=0.025 - NOSE_OPENSTACK_SHOW_ELAPSED=1 - NOSE_OPENSTACK_STDOUT=1 + LANG=en_US.UTF-8 + LANGUAGE=en_US:en + LC_ALL=C + OS_STDOUT_NOCAPTURE=False + OS_STDERR_NOCAPTURE=False + deps = -r{toxinidir}/tools/pip-requires -r{toxinidir}/tools/test-requires -commands = nosetests {posargs} - -[tox:jenkins] -downloadcache = ~/cache/pip +commands = python setup.py testr --testr-args='{posargs}' [testenv:pep8] commands = pep8 --repeat --show-source --ignore=E711,E712,E125,E126 --exclude=.venv,.tox,dist,doc . -[testenv:cover] -setenv = NOSE_WITH_COVERAGE=1 - [testenv:venv] commands = {posargs} + +[testenv:cover] +commands = python setup.py testr --coverage --testr-args='{posargs}' + +[tox:jenkins] +downloadcache = ~/cache/pip