Browse Source

Refactor test framework

Manila's test framework is pretty old and requires update.

Changes:
- usage of nose replaced with testr
- now all the tests are thread safe
- added new options for run_tests.sh, such as --concurrency, --debug, etc...
- new '--concurrency' option for run_tests.sh defaults to 1, examples:
    ./run_tests.sh  # will run in 1 thread
    ./run_tests.sh --concurrency 2  # will run tests in 2 threads
- added tools/colorizer.py for colorizing output of testrun with run_tests.sh
- tests running with tox use as much threads as cores available by default
- examples of testrun with tox:
    tox  # will run test suites defined with 'envlist' in tox.ini, now it is pep8,py26,py27
    tox -epy27  # amount of threads is equal to amount of cores
    tox -epy27 -- --concurrency=2  # amount of threads is 2
    tox -epy27 -- --concurrency=4  # amount of threads is 4
- Added 'Database' class to manila.test module, for more conveniant db testing
- updated policy file 'manila/tests/policy.json' to allow share-network actions
- removed nose-related requirements
- added new requirements for testrepository, subunit

With merge of this change all old installed virtual environments become
incompatible and should be removed with "rm -rf .tox .venv" before testrun.

Implements blueprint testr-with-unittests

Change-Id: I9579ecd538e29d478dbc12adc7dcc33fc668b397
vponomaryov 4 years ago
parent
commit
16a04df3d0

+ 1
- 0
.gitignore View File

@@ -24,6 +24,7 @@ instances
24 24
 keeper
25 25
 keys
26 26
 local_settings.py
27
+subunit.log
27 28
 tools/conf/manila.conf*
28 29
 tools/lintstack.head.py
29 30
 tools/pylint_exceptions

+ 7
- 0
.testr.conf View File

@@ -0,0 +1,7 @@
1
+[DEFAULT]
2
+test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
3
+             OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
4
+             OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \
5
+             ${PYTHON:-python} -m subunit.run discover -t ./ ./manila/tests $LISTOPT $IDOPTION
6
+test_id_option=--load-list $IDFILE
7
+test_list_option=--list

+ 10
- 0
HACKING.rst View File

@@ -233,6 +233,16 @@ The copy of the code should never be directly modified here. Please
233 233
 always update openstack-common first and then run the script to copy
234 234
 the changes across.
235 235
 
236
+
237
+Running Tests
238
+-------------
239
+The testing system is based on a combination of tox and testr. If you just
240
+want to run the whole suite, run `tox` and all will be fine. However, if
241
+you'd like to dig in a bit more, you might want to learn some things about
242
+testr itself. A basic walkthrough for OpenStack can be found at
243
+http://wiki.openstack.org/testr
244
+
245
+
236 246
 OpenStack Trademark
237 247
 -------------------
238 248
 

+ 4
- 3
manila/api/openstack/__init__.py View File

@@ -20,7 +20,6 @@ WSGI middleware for OpenStack API controllers.
20 20
 
21 21
 import routes
22 22
 
23
-from manila.api.middleware import fault
24 23
 from manila.api.openstack import wsgi
25 24
 from manila.openstack.common import log as logging
26 25
 from manila import utils
@@ -123,8 +122,10 @@ class APIRouter(base_wsgi.Router):
123 122
         raise NotImplementedError
124 123
 
125 124
 
126
-class FaultWrapper(fault.FaultWrapper):
125
+class FaultWrapper(base_wsgi.Middleware):
127 126
     def __init__(self, application):
128 127
         LOG.warn(_('manila.api.openstack:FaultWrapper is deprecated. Please '
129 128
                    'use manila.api.middleware.fault:FaultWrapper instead.'))
130
-        super(FaultWrapper, self).__init__(application)
129
+        # Avoid circular imports from here.
130
+        from manila.api.middleware import fault
131
+        super(FaultWrapper, self).__init__(fault.FaultWrapper(application))

+ 59
- 1
manila/test.py View File

@@ -22,14 +22,19 @@ inline callbacks.
22 22
 """
23 23
 
24 24
 import functools
25
+import os
26
+import shutil
25 27
 import uuid
26 28
 
29
+import fixtures
27 30
 import mock
28 31
 from oslo.config import cfg
29 32
 from oslo.messaging import conffixture as messaging_conffixture
30 33
 import six
31 34
 import testtools
32 35
 
36
+from manila.db import migration
37
+from manila.db.sqlalchemy import session as sqla_session
33 38
 from manila.openstack.common import importutils
34 39
 from manila.openstack.common import log as logging
35 40
 from manila.openstack.common import timeutils
@@ -52,6 +57,47 @@ CONF.register_opts(test_opts)
52 57
 
53 58
 LOG = logging.getLogger(__name__)
54 59
 
60
+_DB_CACHE = None
61
+
62
+
63
+class Database(fixtures.Fixture):
64
+
65
+    def __init__(self, db_session, db_migrate, sql_connection, sqlite_db,
66
+                 sqlite_clean_db):
67
+        self.sql_connection = sql_connection
68
+        self.sqlite_db = sqlite_db
69
+        self.sqlite_clean_db = sqlite_clean_db
70
+        self.engine = db_session.get_engine()
71
+        self.engine.dispose()
72
+        conn = self.engine.connect()
73
+        if sql_connection == "sqlite://":
74
+            if db_migrate.db_version() > db_migrate.INIT_VERSION:
75
+                return
76
+        else:
77
+            testdb = os.path.join(CONF.state_path, sqlite_db)
78
+            if os.path.exists(testdb):
79
+                return
80
+        db_migrate.db_sync()
81
+        if sql_connection == "sqlite://":
82
+            conn = self.engine.connect()
83
+            self._DB = "".join(line for line in conn.connection.iterdump())
84
+            self.engine.dispose()
85
+        else:
86
+            cleandb = os.path.join(CONF.state_path, sqlite_clean_db)
87
+            shutil.copyfile(testdb, cleandb)
88
+
89
+    def setUp(self):
90
+        super(Database, self).setUp()
91
+        if self.sql_connection == "sqlite://":
92
+            conn = self.engine.connect()
93
+            conn.connection.executescript(self._DB)
94
+            self.addCleanup(self.engine.dispose)  # pylint: disable=E1101
95
+        else:
96
+            shutil.copyfile(
97
+                os.path.join(CONF.state_path, self.sqlite_clean_db),
98
+                os.path.join(CONF.state_path, self.sqlite_db),
99
+            )
100
+
55 101
 
56 102
 class StubOutForTesting(object):
57 103
     def __init__(self, parent):
@@ -77,7 +123,19 @@ class TestCase(testtools.TestCase):
77 123
         #             now that we have some required db setup for the system
78 124
         #             to work properly.
79 125
         self.start = timeutils.utcnow()
80
-        tests.reset_db()
126
+
127
+        self.log_fixture = self.useFixture(fixtures.FakeLogger())
128
+
129
+        global _DB_CACHE
130
+        if not _DB_CACHE:
131
+            _DB_CACHE = Database(
132
+                sqla_session,
133
+                migration,
134
+                sql_connection=CONF.sql_connection,
135
+                sqlite_db=CONF.sqlite_db,
136
+                sqlite_clean_db=CONF.sqlite_clean_db,
137
+            )
138
+        self.useFixture(_DB_CACHE)
81 139
 
82 140
         self.stubs = StubOutForTesting(self)
83 141
         self.injected = []

+ 0
- 44
manila/tests/__init__.py View File

@@ -30,52 +30,8 @@
30 30
 
31 31
 import eventlet
32 32
 eventlet.monkey_patch()
33
-from oslo.config import cfg
34 33
 
35 34
 # See http://code.google.com/p/python-nose/issues/detail?id=373
36 35
 # The code below enables nosetests to work with i18n _() blocks
37 36
 import __builtin__
38 37
 setattr(__builtin__, '_', lambda x: x)
39
-import os
40
-import shutil
41
-
42
-from manila.db import migration
43
-from manila.db.sqlalchemy.session import get_engine
44
-from manila.tests import conf_fixture
45
-
46
-
47
-CONF = cfg.CONF
48
-
49
-_DB = None
50
-
51
-
52
-def reset_db():
53
-    if CONF.sql_connection == "sqlite://":
54
-        engine = get_engine()
55
-        engine.dispose()
56
-        conn = engine.connect()
57
-        conn.connection.executescript(_DB)
58
-    else:
59
-        shutil.copyfile(os.path.join(CONF.state_path, CONF.sqlite_clean_db),
60
-                        os.path.join(CONF.state_path, CONF.sqlite_db))
61
-
62
-
63
-def setup():
64
-    conf_fixture.set_defaults(CONF)
65
-    if CONF.sql_connection == "sqlite://":
66
-        if migration.db_version() > 1:
67
-            return
68
-    else:
69
-        testdb = os.path.join(CONF.state_path, CONF.sqlite_db)
70
-        if os.path.exists(testdb):
71
-            return
72
-    migration.db_sync()
73
-
74
-    if CONF.sql_connection == "sqlite://":
75
-        global _DB
76
-        engine = get_engine()
77
-        conn = engine.connect()
78
-        _DB = "".join(line for line in conn.connection.iterdump())
79
-    else:
80
-        cleandb = os.path.join(CONF.state_path, CONF.sqlite_clean_db)
81
-        shutil.copyfile(testdb, cleandb)

+ 6
- 0
manila/tests/policy.json View File

@@ -13,6 +13,12 @@
13 13
     "share:get_snapshot": [],
14 14
     "share:get_all_snapshots": [],
15 15
 
16
+    "share_network:create": [],
17
+    "share_network:index": [],
18
+    "share_network:show": [],
19
+    "share_network:update": [],
20
+    "share_network:delete": [],
21
+
16 22
     "share_server:index": [["rule:admin_api"]],
17 23
     "share_server:show": [["rule:admin_api"]],
18 24
     "share_server:details": [["rule:admin_api"]],

+ 1
- 1
manila/tests/test_share_rpcapi.py View File

@@ -33,6 +33,7 @@ CONF = cfg.CONF
33 33
 class ShareRpcAPITestCase(test.TestCase):
34 34
 
35 35
     def setUp(self):
36
+        super(ShareRpcAPITestCase, self).setUp()
36 37
         self.context = context.get_admin_context()
37 38
         shr = {}
38 39
         shr['host'] = 'fake_host'
@@ -53,7 +54,6 @@ class ShareRpcAPITestCase(test.TestCase):
53 54
         self.fake_access = jsonutils.to_primitive(access)
54 55
         self.fake_snapshot = jsonutils.to_primitive(snapshot)
55 56
         self.fake_share_server = jsonutils.to_primitive(share_server)
56
-        super(ShareRpcAPITestCase, self).setUp()
57 57
         self.ctxt = context.RequestContext('fake_user', 'fake_project')
58 58
         self.rpcapi = share_rpcapi.ShareAPI()
59 59
 

+ 141
- 83
run_tests.sh View File

@@ -1,25 +1,33 @@
1 1
 #!/bin/bash
2 2
 
3
-set -u
3
+set -eu
4 4
 
5 5
 function usage {
6 6
   echo "Usage: $0 [OPTION]..."
7 7
   echo "Run Manila's test suite(s)"
8 8
   echo ""
9
-  echo "  -V, --virtual-env        Always use virtualenv.  Install automatically if not present"
10
-  echo "  -N, --no-virtual-env     Don't use virtualenv.  Run tests in local environment"
11
-  echo "  -s, --no-site-packages   Isolate the virtualenv from the global Python environment"
12
-  echo "  -r, --recreate-db        Recreate the test database (deprecated, as this is now the default)."
13
-  echo "  -n, --no-recreate-db     Don't recreate the test database."
14
-  echo "  -x, --stop               Stop running tests after the first error or failure."
15
-  echo "  -f, --force              Force a clean re-build of the virtual environment. Useful when dependencies have been added."
16
-  echo "  -u, --update             Update the virtual environment with any     newer package versions"
17
-  echo "  -p, --pep8               Just run PEP8 and HACKING compliance check"
18
-  echo "  -P, --no-pep8            Don't run static code checks"
19
-  echo "  -c, --coverage           Generate coverage report"
20
-  echo "  -X, --coverage-xml       Generate XML coverage report."
21
-  echo "  -h, --help               Print this usage message"
22
-  echo "  --hide-elapsed           Don't print the elapsed time for each test along with slow test list"
9
+  echo "  -V, --virtual-env           Always use virtualenv.  Install automatically if not present"
10
+  echo "  -N, --no-virtual-env        Don't use virtualenv.  Run tests in local environment"
11
+  echo "  -s, --no-site-packages      Isolate the virtualenv from the global Python environment"
12
+  echo "  -r, --recreate-db           Recreate the test database (deprecated, as this is now the default)."
13
+  echo "  -n, --no-recreate-db        Don't recreate the test database."
14
+  echo "  -f, --force                 Force a clean re-build of the virtual environment. Useful when dependencies have been added."
15
+  echo "  -u, --update                Update the virtual environment with any     newer package versions"
16
+  echo "  -p, --pep8                  Just run PEP8 and HACKING compliance check"
17
+  echo "  -P, --no-pep8               Don't run static code checks"
18
+  echo "  -c, --coverage              Generate coverage report"
19
+  echo "  -d, --debug                 Run tests with testtools instead of testr. This allows you to use the debugger."
20
+  echo "  -h, --help                  Print this usage message"
21
+  echo "  --hide-elapsed              Don't print the elapsed time for each test along with slow test list"
22
+  echo "  --virtual-env-path <path>   Location of the virtualenv directory."
23
+  echo "                                  Default: \$(pwd)"
24
+  echo "  --virtual-env-name <name>   Name of the virtualenv directory."
25
+  echo "                                  Default: .venv"
26
+  echo "  --tools-path <dir>          Location of the tools directory."
27
+  echo "                                  Default: \$(pwd)"
28
+  echo "  --concurrency <concurrency> How many processes to use when running the tests."
29
+  echo "                                  A value of 0 autodetects concurrency from your CPU count."
30
+  echo "                                  Default: 1"
23 31
   echo ""
24 32
   echo "Note: with no options specified, the script will try to run the tests in a virtual environment,"
25 33
   echo "      If no virtualenv is found, the script will ask if you would like to create one.  If you "
@@ -27,94 +35,152 @@ function usage {
27 35
   exit
28 36
 }
29 37
 
30
-function process_option {
31
-  case "$1" in
32
-    -h|--help) usage;;
33
-    -V|--virtual-env) always_venv=1; never_venv=0;;
34
-    -N|--no-virtual-env) always_venv=0; never_venv=1;;
35
-    -s|--no-site-packages) no_site_packages=1;;
36
-    -r|--recreate-db) recreate_db=1;;
37
-    -n|--no-recreate-db) recreate_db=0;;
38
-    -m|--patch-migrate) patch_migrate=1;;
39
-    -w|--no-patch-migrate) patch_migrate=0;;
40
-    -f|--force) force=1;;
41
-    -u|--update) update=1;;
42
-    -p|--pep8) just_pep8=1;;
43
-    -P|--no-pep8) no_pep8=1;;
44
-    -c|--coverage) coverage=1;;
45
-    -X|--coverage-xml) coverage_xml=1;;
46
-    -*) noseopts="$noseopts $1";;
47
-    *) noseargs="$noseargs $1"
48
-  esac
38
+function process_options {
39
+  i=1
40
+  while [ $i -le $# ]; do
41
+    case "${!i}" in
42
+      -h|--help) usage;;
43
+      -V|--virtual-env) always_venv=1; never_venv=0;;
44
+      -N|--no-virtual-env) always_venv=0; never_venv=1;;
45
+      -s|--no-site-packages) no_site_packages=1;;
46
+      -r|--recreate-db) recreate_db=1;;
47
+      -n|--no-recreate-db) recreate_db=0;;
48
+      -f|--force) force=1;;
49
+      -u|--update) update=1;;
50
+      -p|--pep8) just_pep8=1;;
51
+      -P|--no-pep8) no_pep8=1;;
52
+      -c|--coverage) coverage=1;;
53
+      -d|--debug) debug=1;;
54
+      --virtual-env-path)
55
+        (( i++ ))
56
+        venv_path=${!i}
57
+        ;;
58
+      --virtual-env-name)
59
+        (( i++ ))
60
+        venv_dir=${!i}
61
+        ;;
62
+      --tools-path)
63
+        (( i++ ))
64
+        tools_path=${!i}
65
+        ;;
66
+      --concurrency)
67
+        (( i++ ))
68
+        concurrency=${!i}
69
+        ;;
70
+      -*) testropts="$testropts ${!i}";;
71
+      *) testrargs="$testrargs ${!i}"
72
+    esac
73
+    (( i++ ))
74
+  done
49 75
 }
50 76
 
51
-venv=.venv
77
+tool_path=${tools_path:-$(pwd)}
78
+venv_path=${venv_path:-$(pwd)}
79
+venv_dir=${venv_name:-.venv}
52 80
 with_venv=tools/with_venv.sh
53 81
 always_venv=0
54 82
 never_venv=0
55 83
 force=0
56 84
 no_site_packages=0
57 85
 installvenvopts=
58
-noseargs=
59
-noseopts=
86
+testrargs=
87
+testropts=
60 88
 wrapper=""
61 89
 just_pep8=0
62 90
 no_pep8=0
63 91
 coverage=0
64
-coverage_xml=0
92
+debug=0
65 93
 recreate_db=1
66
-patch_migrate=1
67 94
 update=0
95
+concurrency=1
68 96
 
69
-export NOSE_WITH_OPENSTACK=true
70
-export NOSE_OPENSTACK_COLOR=true
71
-export NOSE_OPENSTACK_SHOW_ELAPSED=true
72
-
73
-for arg in "$@"; do
74
-  process_option $arg
75
-done
76
-
77
-# If enabled, tell nose to collect coverage data
78
-if [ $coverage -eq 1 ]; then
79
-    noseopts="$noseopts --with-coverage --cover-package=manila"
80
-fi
81
-if [ $coverage_xml -eq 1 ]; then
82
-    noseopts="$noseopts --with-xcoverage --cover-package=manila --xcoverage-file=`pwd`/coverage.xml"
83
-fi
97
+process_options $@
98
+# Make our paths available to other scripts we call
99
+export venv_path
100
+export venv_dir
101
+export venv_name
102
+export tools_dir
103
+export venv=${venv_path}/${venv_dir}
84 104
 
85 105
 if [ $no_site_packages -eq 1 ]; then
86 106
   installvenvopts="--no-site-packages"
87 107
 fi
88 108
 
109
+function init_testr {
110
+  if [ ! -d .testrepository ]; then
111
+    ${wrapper} testr init
112
+  fi
113
+}
114
+
89 115
 function run_tests {
90 116
   # Cleanup *pyc
91 117
   ${wrapper} find . -type f -name "*.pyc" -delete
118
+
119
+  if [ $debug -eq 1 ]; then
120
+    if [ "$testropts" = "" ] && [ "$testrargs" = "" ]; then
121
+      # Default to running all tests if specific test is not
122
+      # provided.
123
+      testrargs="discover ./manila/tests"
124
+    fi
125
+    ${wrapper} python -m testtools.run $testropts $testrargs
126
+
127
+    # Short circuit because all of the testr and coverage stuff
128
+    # below does not make sense when running testtools.run for
129
+    # debugging purposes.
130
+    return $?
131
+  fi
132
+
133
+  if [ $coverage -eq 1 ]; then
134
+    TESTRTESTS="$TESTRTESTS --coverage"
135
+  else
136
+    TESTRTESTS="$TESTRTESTS"
137
+  fi
138
+
92 139
   # Just run the test suites in current environment
93
-  ${wrapper} $NOSETESTS
140
+  set +e
141
+  testrargs=`echo "$testrargs" | sed -e's/^\s*\(.*\)\s*$/\1/'`
142
+  TESTRTESTS="$TESTRTESTS --testr-args='--subunit --concurrency $concurrency $testropts $testrargs'"
143
+  if [ setup.cfg -nt manila.egg-info/entry_points.txt ]
144
+  then
145
+    ${wrapper} python setup.py egg_info
146
+  fi
147
+  echo "Running \`${wrapper} $TESTRTESTS\`"
148
+  if ${wrapper} which subunit-2to1 2>&1 > /dev/null
149
+  then
150
+    # subunit-2to1 is present, testr subunit stream should be in version 2
151
+    # format. Convert to version one before colorizing.
152
+    bash -c "${wrapper} $TESTRTESTS | ${wrapper} subunit-2to1 | ${wrapper} tools/colorizer.py"
153
+  else
154
+    bash -c "${wrapper} $TESTRTESTS | ${wrapper} tools/colorizer.py"
155
+  fi
94 156
   RESULT=$?
157
+  set -e
158
+
159
+  copy_subunit_log
160
+
161
+  if [ $coverage -eq 1 ]; then
162
+    echo "Generating coverage report in covhtml/"
163
+    # Don't compute coverage for common code, which is tested elsewhere
164
+    ${wrapper} coverage combine
165
+    ${wrapper} coverage html --include='manila/*' --omit='manila/openstack/common/*' -d covhtml -i
166
+  fi
167
+
95 168
   return $RESULT
96 169
 }
97 170
 
98
-# Files of interest
99
-# NOTE(lzyeval): Avoid selecting manila-api-paste.ini and manila.conf in manila/bin
100
-#                when running on devstack.
101
-# NOTE(lzyeval): Avoid selecting *.pyc files to reduce pep8 check-up time
102
-#                when running on devstack.
103
-# NOTE(dprince): Exclude xenapi plugins. They are Python 2.4 code and as such
104
-#                cannot be expected to work with tools/hacking checks.
105
-xen_net_path="plugins/xenserver/networking/etc/xensource/scripts"
106
-srcfiles=`find manila -type f -name "*.py" ! -path "manila/openstack/common/*"`
107
-srcfiles+=" `find bin -type f ! -name "manila.conf*" ! -name "*api-paste.ini*"`"
108
-srcfiles+=" `find tools -type f -name "*.py"`"
109
-srcfiles+=" setup.py"
171
+function copy_subunit_log {
172
+  LOGNAME=`cat .testrepository/next-stream`
173
+  LOGNAME=$(($LOGNAME - 1))
174
+  LOGNAME=".testrepository/${LOGNAME}"
175
+  cp $LOGNAME subunit.log
176
+}
110 177
 
111 178
 function run_pep8 {
112
-  echo "Running PEP8 and HACKING compliance check..."
113
-  bash -c "${wrapper} flake8 manila* bin"
179
+  echo "Running flake8..."
180
+  bash -c "${wrapper} flake8"
114 181
 }
115 182
 
116
-
117
-NOSETESTS="nosetests $noseopts $noseargs"
183
+TESTRTESTS="python -m manila.openstack.common.lockutils python setup.py testr"
118 184
 
119 185
 if [ $never_venv -eq 0 ]
120 186
 then
@@ -160,23 +226,15 @@ if [ $recreate_db -eq 1 ]; then
160 226
     rm -f tests.sqlite
161 227
 fi
162 228
 
229
+init_testr
163 230
 run_tests
164
-RET=$?
165 231
 
166 232
 # NOTE(sirp): we only want to run pep8 when we're running the full-test suite,
167 233
 # not when we're running tests individually. To handle this, we need to
168
-# distinguish between options (noseopts), which begin with a '-', and
169
-# arguments (noseargs).
170
-if [ -z "$noseargs" ]; then
234
+# distinguish between options (testropts), which begin with a '-', and
235
+# arguments (testrargs).
236
+if [ -z "$testrargs" ]; then
171 237
   if [ $no_pep8 -eq 0 ]; then
172 238
     run_pep8
173 239
   fi
174 240
 fi
175
-
176
-if [ $coverage -eq 1 ]; then
177
-    echo "Generating coverage report in covhtml/"
178
-    # Don't compute coverage for common code, which is tested elsewhere
179
-    ${wrapper} coverage html --include='manila/*' --omit='manila/openstack/common/*' -d covhtml -i
180
-fi
181
-
182
-exit $RET

+ 0
- 8
setup.cfg View File

@@ -76,13 +76,5 @@ keywords = _ gettext ngettext l_ lazy_gettext
76 76
 mapping_file = babel.cfg
77 77
 output_file = manila/locale/manila.pot
78 78
 
79
-[nosetests]
80
-tests=manila/tests
81
-cover-package = manila
82
-cover-erase = true
83
-cover-inclusive = true
84
-verbosity=2
85
-detailed-errors=1
86
-
87 79
 [wheel]
88 80
 universal = 1

+ 3
- 4
test-requirements.txt View File

@@ -1,13 +1,12 @@
1 1
 hacking>=0.9.2,<0.10
2 2
 
3 3
 coverage>=3.6
4
+discover
4 5
 fixtures>=0.3.14
5 6
 mock>=1.0
6 7
 MySQL-python
7
-nose
8
-nosehtmloutput>=0.0.3
9
-nosexcover
10
-openstack.nose_plugin>=0.7
11 8
 psycopg2
9
+python-subunit
12 10
 sphinx>=1.1.2,!=1.2.0,<1.3
11
+testrepository>=0.0.18
13 12
 testtools>=0.9.34

+ 332
- 0
tools/colorizer.py View File

@@ -0,0 +1,332 @@
1
+#!/usr/bin/env python
2
+# Copyright (c) 2013, Nebula, Inc.
3
+# Copyright 2010 United States Government as represented by the
4
+# Administrator of the National Aeronautics and Space Administration.
5
+# All Rights Reserved.
6
+#
7
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
8
+#    not use this file except in compliance with the License. You may obtain
9
+#    a copy of the License at
10
+#
11
+#         http://www.apache.org/licenses/LICENSE-2.0
12
+#
13
+#    Unless required by applicable law or agreed to in writing, software
14
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16
+#    License for the specific language governing permissions and limitations
17
+#    under the License.
18
+#
19
+# Colorizer Code is borrowed from Twisted:
20
+# Copyright (c) 2001-2010 Twisted Matrix Laboratories.
21
+#
22
+#    Permission is hereby granted, free of charge, to any person obtaining
23
+#    a copy of this software and associated documentation files (the
24
+#    "Software"), to deal in the Software without restriction, including
25
+#    without limitation the rights to use, copy, modify, merge, publish,
26
+#    distribute, sublicense, and/or sell copies of the Software, and to
27
+#    permit persons to whom the Software is furnished to do so, subject to
28
+#    the following conditions:
29
+#
30
+#    The above copyright notice and this permission notice shall be
31
+#    included in all copies or substantial portions of the Software.
32
+#
33
+#    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
34
+#    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
35
+#    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
36
+#    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
37
+#    LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
38
+#    OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
39
+#    WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
40
+
41
+"""Display a subunit stream through a colorized unittest test runner."""
42
+
43
+import heapq
44
+import sys
45
+import unittest
46
+
47
+import subunit
48
+import testtools
49
+
50
+
51
+class _AnsiColorizer(object):
52
+    """ANSI colorizer that wraps a stream object.
53
+
54
+    colorizer is an object that loosely wraps around a stream, allowing
55
+    callers to write text to the stream in a particular color.
56
+
57
+    Colorizer classes must implement C{supported()} and C{write(text, color)}.
58
+    """
59
+    _colors = dict(black=30, red=31, green=32, yellow=33,
60
+                   blue=34, magenta=35, cyan=36, white=37)
61
+
62
+    def __init__(self, stream):
63
+        self.stream = stream
64
+
65
+    def supported(cls, stream=sys.stdout):
66
+        """Check if platform is supported.
67
+
68
+        A class method that returns True if the current platform supports
69
+        coloring terminal output using this method.
70
+
71
+        Returns False otherwise.
72
+        """
73
+        if not stream.isatty():
74
+            return False  # auto color only on TTYs
75
+        try:
76
+            import curses
77
+        except ImportError:
78
+            return False
79
+        else:
80
+            try:
81
+                try:
82
+                    return curses.tigetnum("colors") > 2
83
+                except curses.error:
84
+                    curses.setupterm()
85
+                    return curses.tigetnum("colors") > 2
86
+            except Exception:
87
+                # guess false in case of error
88
+                return False
89
+    supported = classmethod(supported)
90
+
91
+    def write(self, text, color):
92
+        """Write the given text to the stream in the given color.
93
+
94
+        @param text: Text to be written to the stream.
95
+
96
+        @param color: A string label for a color. e.g. 'red', 'white'.
97
+        """
98
+        color = self._colors[color]
99
+        self.stream.write('\x1b[%s;1m%s\x1b[0m' % (color, text))
100
+
101
+
102
+class _Win32Colorizer(object):
103
+    """See _AnsiColorizer docstring."""
104
+    def __init__(self, stream):
105
+        import win32console
106
+        red, green, blue, bold = (win32console.FOREGROUND_RED,
107
+                                  win32console.FOREGROUND_GREEN,
108
+                                  win32console.FOREGROUND_BLUE,
109
+                                  win32console.FOREGROUND_INTENSITY)
110
+        self.stream = stream
111
+        self.screenBuffer = win32console.GetStdHandle(
112
+            win32console.STD_OUT_HANDLE)
113
+        self._colors = {
114
+            'normal': red | green | blue,
115
+            'red': red | bold,
116
+            'green': green | bold,
117
+            'blue': blue | bold,
118
+            'yellow': red | green | bold,
119
+            'magenta': red | blue | bold,
120
+            'cyan': green | blue | bold,
121
+            'white': red | green | blue | bold
122
+        }
123
+
124
+    def supported(cls, stream=sys.stdout):
125
+        try:
126
+            import win32console
127
+            screenBuffer = win32console.GetStdHandle(
128
+                win32console.STD_OUT_HANDLE)
129
+        except ImportError:
130
+            return False
131
+        import pywintypes
132
+        try:
133
+            screenBuffer.SetConsoleTextAttribute(
134
+                win32console.FOREGROUND_RED |
135
+                win32console.FOREGROUND_GREEN |
136
+                win32console.FOREGROUND_BLUE)
137
+        except pywintypes.error:
138
+            return False
139
+        else:
140
+            return True
141
+    supported = classmethod(supported)
142
+
143
+    def write(self, text, color):
144
+        color = self._colors[color]
145
+        self.screenBuffer.SetConsoleTextAttribute(color)
146
+        self.stream.write(text)
147
+        self.screenBuffer.SetConsoleTextAttribute(self._colors['normal'])
148
+
149
+
150
+class _NullColorizer(object):
151
+    """See _AnsiColorizer docstring."""
152
+    def __init__(self, stream):
153
+        self.stream = stream
154
+
155
+    def supported(cls, stream=sys.stdout):
156
+        return True
157
+    supported = classmethod(supported)
158
+
159
+    def write(self, text, color):
160
+        self.stream.write(text)
161
+
162
+
163
+def get_elapsed_time_color(elapsed_time):
164
+    if elapsed_time > 1.0:
165
+        return 'red'
166
+    elif elapsed_time > 0.25:
167
+        return 'yellow'
168
+    else:
169
+        return 'green'
170
+
171
+
172
+class ManilaTestResult(testtools.TestResult):
173
+    def __init__(self, stream, descriptions, verbosity):
174
+        super(ManilaTestResult, self).__init__()
175
+        self.stream = stream
176
+        self.showAll = verbosity > 1
177
+        self.num_slow_tests = 10
178
+        self.slow_tests = []  # this is a fixed-sized heap
179
+        self.colorizer = None
180
+        # NOTE(vish): reset stdout for the terminal check
181
+        stdout = sys.stdout
182
+        sys.stdout = sys.__stdout__
183
+        for colorizer in [_Win32Colorizer, _AnsiColorizer, _NullColorizer]:
184
+            if colorizer.supported():
185
+                self.colorizer = colorizer(self.stream)
186
+                break
187
+        sys.stdout = stdout
188
+        self.start_time = None
189
+        self.last_time = {}
190
+        self.results = {}
191
+        self.last_written = None
192
+
193
+    def _writeElapsedTime(self, elapsed):
194
+        color = get_elapsed_time_color(elapsed)
195
+        self.colorizer.write("  %.2f" % elapsed, color)
196
+
197
+    def _addResult(self, test, *args):
198
+        try:
199
+            name = test.id()
200
+        except AttributeError:
201
+            name = 'Unknown.unknown'
202
+        test_class, test_name = name.rsplit('.', 1)
203
+
204
+        elapsed = (self._now() - self.start_time).total_seconds()
205
+        item = (elapsed, test_class, test_name)
206
+        if len(self.slow_tests) >= self.num_slow_tests:
207
+            heapq.heappushpop(self.slow_tests, item)
208
+        else:
209
+            heapq.heappush(self.slow_tests, item)
210
+
211
+        self.results.setdefault(test_class, [])
212
+        self.results[test_class].append((test_name, elapsed) + args)
213
+        self.last_time[test_class] = self._now()
214
+        self.writeTests()
215
+
216
+    def _writeResult(self, test_name, elapsed, long_result, color,
217
+                     short_result, success):
218
+        if self.showAll:
219
+            self.stream.write('    %s' % str(test_name).ljust(66))
220
+            self.colorizer.write(long_result, color)
221
+            if success:
222
+                self._writeElapsedTime(elapsed)
223
+            self.stream.writeln()
224
+        else:
225
+            self.colorizer.write(short_result, color)
226
+
227
+    def addSuccess(self, test):
228
+        super(ManilaTestResult, self).addSuccess(test)
229
+        self._addResult(test, 'OK', 'green', '.', True)
230
+
231
+    def addFailure(self, test, err):
232
+        if test.id() == 'process-returncode':
233
+            return
234
+        super(ManilaTestResult, self).addFailure(test, err)
235
+        self._addResult(test, 'FAIL', 'red', 'F', False)
236
+
237
+    def addError(self, test, err):
238
+        super(ManilaTestResult, self).addFailure(test, err)
239
+        self._addResult(test, 'ERROR', 'red', 'E', False)
240
+
241
+    def addSkip(self, test, reason=None, details=None):
242
+        super(ManilaTestResult, self).addSkip(test, reason, details)
243
+        self._addResult(test, 'SKIP', 'blue', 'S', True)
244
+
245
+    def startTest(self, test):
246
+        self.start_time = self._now()
247
+        super(ManilaTestResult, self).startTest(test)
248
+
249
+    def writeTestCase(self, cls):
250
+        if not self.results.get(cls):
251
+            return
252
+        if cls != self.last_written:
253
+            self.colorizer.write(cls, 'white')
254
+            self.stream.writeln()
255
+        for result in self.results[cls]:
256
+            self._writeResult(*result)
257
+        del self.results[cls]
258
+        self.stream.flush()
259
+        self.last_written = cls
260
+
261
+    def writeTests(self):
262
+        time = self.last_time.get(self.last_written, self._now())
263
+        if not self.last_written or (self._now() - time).total_seconds() > 2.0:
264
+            diff = 3.0
265
+            while diff > 2.0:
266
+                classes = self.results.keys()
267
+                oldest = min(classes, key=lambda x: self.last_time[x])
268
+                diff = (self._now() - self.last_time[oldest]).total_seconds()
269
+                self.writeTestCase(oldest)
270
+        else:
271
+            self.writeTestCase(self.last_written)
272
+
273
+    def done(self):
274
+        self.stopTestRun()
275
+
276
+    def stopTestRun(self):
277
+        for cls in list(self.results.iterkeys()):
278
+            self.writeTestCase(cls)
279
+        self.stream.writeln()
280
+        self.writeSlowTests()
281
+
282
+    def writeSlowTests(self):
283
+        # Pare out 'fast' tests
284
+        slow_tests = [item for item in self.slow_tests
285
+                      if get_elapsed_time_color(item[0]) != 'green']
286
+        if slow_tests:
287
+            slow_total_time = sum(item[0] for item in slow_tests)
288
+            slow = ("Slowest %i tests took %.2f secs:"
289
+                    % (len(slow_tests), slow_total_time))
290
+            self.colorizer.write(slow, 'yellow')
291
+            self.stream.writeln()
292
+            last_cls = None
293
+            # sort by name
294
+            for elapsed, cls, name in sorted(slow_tests,
295
+                                             key=lambda x: x[1] + x[2]):
296
+                if cls != last_cls:
297
+                    self.colorizer.write(cls, 'white')
298
+                    self.stream.writeln()
299
+                last_cls = cls
300
+                self.stream.write('    %s' % str(name).ljust(68))
301
+                self._writeElapsedTime(elapsed)
302
+                self.stream.writeln()
303
+
304
+    def printErrors(self):
305
+        if self.showAll:
306
+            self.stream.writeln()
307
+        self.printErrorList('ERROR', self.errors)
308
+        self.printErrorList('FAIL', self.failures)
309
+
310
+    def printErrorList(self, flavor, errors):
311
+        for test, err in errors:
312
+            self.colorizer.write("=" * 70, 'red')
313
+            self.stream.writeln()
314
+            self.colorizer.write(flavor, 'red')
315
+            self.stream.writeln(": %s" % test.id())
316
+            self.colorizer.write("-" * 70, 'red')
317
+            self.stream.writeln()
318
+            self.stream.writeln("%s" % err)
319
+
320
+
321
+test = subunit.ProtocolTestCase(sys.stdin, passthrough=None)
322
+
323
+if sys.version_info[0:2] <= (2, 6):
324
+    runner = unittest.TextTestRunner(verbosity=2)
325
+else:
326
+    runner = unittest.TextTestRunner(verbosity=2, resultclass=ManilaTestResult)
327
+
328
+if runner.run(test).wasSuccessful():
329
+    exit_code = 0
330
+else:
331
+    exit_code = 1
332
+sys.exit(exit_code)

+ 6
- 3
tools/with_venv.sh View File

@@ -1,4 +1,7 @@
1 1
 #!/bin/bash
2
-TOOLS=`dirname $0`
3
-VENV=$TOOLS/../.venv
4
-source $VENV/bin/activate && $@
2
+tools_path=${tools_path:-$(dirname $0)}
3
+venv_path=${venv_path:-${tools_path}}
4
+venv_dir=${venv_name:-/../.venv}
5
+TOOLS=${tools_path}
6
+VENV=${venv:-${venv_path}/${venv_dir}}
7
+source ${VENV}/bin/activate && "$@"

+ 13
- 14
tox.ini View File

@@ -1,26 +1,22 @@
1 1
 [tox]
2
+minversion = 1.6
3
+skipsdist = True
2 4
 envlist = py26,py27,pep8
3 5
 
4 6
 [testenv]
5 7
 setenv = VIRTUAL_ENV={envdir}
6
-         NOSE_WITH_OPENSTACK=1
7
-         NOSE_OPENSTACK_COLOR=1
8
-         NOSE_OPENSTACK_RED=0.05
9
-         NOSE_OPENSTACK_YELLOW=0.025
10
-         NOSE_OPENSTACK_SHOW_ELAPSED=1
11
-         NOSE_OPENSTACK_STDOUT=1
12
-         LANG=en_US.UTF-8
13
-         LANGUAGE=en_US:en
14
-         LC_ALL=C
15
-
8
+usedevelop = True
9
+install_command = pip install {opts} {packages}
16 10
 deps = -r{toxinidir}/requirements.txt
17 11
        -r{toxinidir}/test-requirements.txt
18
-commands =
19
-  nosetests {posargs}
12
+commands = python setup.py testr --testr-args='{posargs}'
13
+
14
+[tox:jenkins]
15
+downloadcache = ~/cache/pip
20 16
 
21 17
 [testenv:pep8]
22 18
 commands =
23
-  flake8
19
+  flake8 {posargs} . manila/common
24 20
   flake8 --filename=manila* bin
25 21
 
26 22
 [testenv:genconfig]
@@ -31,7 +27,10 @@ commands =
31 27
 commands = {posargs}
32 28
 
33 29
 [testenv:cover]
34
-setenv = NOSE_WITH_COVERAGE=1
30
+setenv = VIRTUAL_ENV={envdir}
31
+commands =
32
+  python setup.py testr --coverage \
33
+    --testr-args='^(?!.*test.*coverage).*$'
35 34
 
36 35
 [testenv:pylint]
37 36
 setenv = VIRTUAL_ENV={envdir}

Loading…
Cancel
Save