From f845891184b17e2c31a2b02dbc9217978abd3242 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Mon, 21 Nov 2011 19:59:50 -0800 Subject: First steps towards consolidating testing infrastructure This commit begins to implement blueprint consolidate-testing-infrastructure by adding a 'testing' subpackage and moving some modules into it. Change-Id: I04bf860bc386bd2016e7dbc5a6f6ef7379a855bb --- HACKING | 165 ---------------- HACKING.rst | 184 ++++++++++++++++++ doc/source/code.rst | 2 +- doc/source/devref/fakes.rst | 4 +- nova/api/ec2/__init__.py | 2 +- nova/auth/ldapdriver.py | 2 +- nova/auth/manager.py | 2 +- nova/fakememcache.py | 59 ------ nova/fakerabbit.py | 153 --------------- nova/rpc/impl_carrot.py | 4 +- nova/test.py | 4 +- nova/testing/README.rst | 44 +++++ nova/testing/__init__.py | 0 nova/testing/fake/__init__.py | 2 + nova/testing/fake/memcache.py | 59 ++++++ nova/testing/fake/rabbit.py | 153 +++++++++++++++ nova/testing/runner.py | 370 ++++++++++++++++++++++++++++++++++++ nova/tests/test_iptables_network.py | 10 +- run_tests.py | 365 ----------------------------------- run_tests.sh | 2 +- 20 files changed, 828 insertions(+), 758 deletions(-) delete mode 100644 HACKING create mode 100644 HACKING.rst delete mode 100644 nova/fakememcache.py delete mode 100644 nova/fakerabbit.py create mode 100644 nova/testing/README.rst create mode 100644 nova/testing/__init__.py create mode 100644 nova/testing/fake/__init__.py create mode 100644 nova/testing/fake/memcache.py create mode 100644 nova/testing/fake/rabbit.py create mode 100644 nova/testing/runner.py delete mode 100644 run_tests.py diff --git a/HACKING b/HACKING deleted file mode 100644 index 962617e9a..000000000 --- a/HACKING +++ /dev/null @@ -1,165 +0,0 @@ -Nova Style Commandments -======================= - -Step 1: Read http://www.python.org/dev/peps/pep-0008/ -Step 2: Read http://www.python.org/dev/peps/pep-0008/ again -Step 3: Read on - - -General -------- -- Put two newlines between top-level code (funcs, classes, etc) -- Put one newline between methods in classes and anywhere else -- Do not write "except:", use "except Exception:" at the very least -- Include your name with TODOs as in "#TODO(termie)" -- Do not name anything the same name as a built-in or reserved word - - -Imports -------- -- Do not import objects, only modules -- Do not import more than one module per line -- Do not make relative imports -- Order your imports by the full module path -- Organize your imports according to the following template - -:: - # vim: tabstop=4 shiftwidth=4 softtabstop=4 - {{stdlib imports in human alphabetical order}} - \n - {{third-party lib imports in human alphabetical order}} - \n - {{nova imports in human alphabetical order}} - \n - \n - {{begin your code}} - - -Human Alphabetical Order Examples ---------------------------------- -:: - import httplib - import logging - import random - import StringIO - import time - import unittest - - import eventlet - import webob.exc - - import nova.api.ec2 - from nova.api import openstack - from nova.auth import users - import nova.flags - from nova.endpoint import cloud - from nova import test - - -Docstrings ----------- - """A one line docstring looks like this and ends in a period.""" - - - """A multiline docstring has a one-line summary, less than 80 characters. - - Then a new paragraph after a newline that explains in more detail any - general information about the function, class or method. Example usages - are also great to have here if it is a complex class for function. After - you have finished your descriptions add an extra newline and close the - quotations. - - When writing the docstring for a class, an extra line should be placed - after the closing quotations. For more in-depth explanations for these - decisions see http://www.python.org/dev/peps/pep-0257/ - - If you are going to describe parameters and return values, use Sphinx, the - appropriate syntax is as follows. - - :param foo: the foo parameter - :param bar: the bar parameter - :returns: return_type -- description of the return value - :returns: description of the return value - :raises: AttributeError, KeyError - - """ - - -Dictionaries/Lists ------------------- - If a dictionary (dict) or list object is longer than 80 characters, its - items should be split with newlines. Embedded iterables should have their - items indented. Additionally, the last item in the dictionary should have - a trailing comma. This increases readability and simplifies future diffs. - - Example: - - my_dictionary = { - "image": { - "name": "Just a Snapshot", - "size": 2749573, - "properties": { - "user_id": 12, - "arch": "x86_64", - }, - "things": [ - "thing_one", - "thing_two", - ], - "status": "ACTIVE", - }, - } - - -Calling Methods ---------------- - Calls to methods 80 characters or longer should format each argument with - newlines. This is not a requirement, but a guideline. - - unnecessarily_long_function_name('string one', - 'string two', - kwarg1=constants.ACTIVE, - kwarg2=['a', 'b', 'c']) - - - Rather than constructing parameters inline, it is better to break things up: - - list_of_strings = [ - 'what_a_long_string', - 'not as long', - ] - - dict_of_numbers = { - 'one': 1, - 'two': 2, - 'twenty four': 24, - } - - object_one.call_a_method('string three', - 'string four', - kwarg1=list_of_strings, - kwarg2=dict_of_numbers) - - -Internationalization (i18n) Strings ------------------------------------ - In order to support multiple languages, we have a mechanism to support - automatic translations of exception and log strings. - - Example: - msg = _("An error occurred") - raise HTTPBadRequest(explanation=msg) - - If you have a variable to place within the string, first internationalize - the template string then do the replacement. - - Example: - msg = _("Missing parameter: %s") % ("flavor",) - LOG.error(msg) - - If you have multiple variables to place in the string, use keyword - parameters. This helps our translators reorder parameters when needed. - - Example: - msg = _("The server with id %(s_id)s has no key %(m_key)s") - LOG.error(msg % {"s_id": "1234", "m_key": "imageId"}) diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 000000000..d9d0f41a6 --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,184 @@ +Nova Style Commandments +======================= + +- Step 1: Read http://www.python.org/dev/peps/pep-0008/ +- Step 2: Read http://www.python.org/dev/peps/pep-0008/ again +- Step 3: Read on + + +General +------- +- Put two newlines between top-level code (funcs, classes, etc) +- Put one newline between methods in classes and anywhere else +- Do not write "except:", use "except Exception:" at the very least +- Include your name with TODOs as in "#TODO(termie)" +- Do not name anything the same name as a built-in or reserved word + + +Imports +------- +- Do not import objects, only modules +- Do not import more than one module per line +- Do not make relative imports +- Order your imports by the full module path +- Organize your imports according to the following template + +Example:: + + # vim: tabstop=4 shiftwidth=4 softtabstop=4 + {{stdlib imports in human alphabetical order}} + \n + {{third-party lib imports in human alphabetical order}} + \n + {{nova imports in human alphabetical order}} + \n + \n + {{begin your code}} + + +Human Alphabetical Order Examples +--------------------------------- +Example:: + + import httplib + import logging + import random + import StringIO + import time + import unittest + + import eventlet + import webob.exc + + import nova.api.ec2 + from nova.api import openstack + from nova.auth import users + import nova.flags + from nova.endpoint import cloud + from nova import test + + +Docstrings +---------- +Example:: + + """A one line docstring looks like this and ends in a period.""" + + + """A multiline docstring has a one-line summary, less than 80 characters. + + Then a new paragraph after a newline that explains in more detail any + general information about the function, class or method. Example usages + are also great to have here if it is a complex class for function. After + you have finished your descriptions add an extra newline and close the + quotations. + + When writing the docstring for a class, an extra line should be placed + after the closing quotations. For more in-depth explanations for these + decisions see http://www.python.org/dev/peps/pep-0257/ + + If you are going to describe parameters and return values, use Sphinx, the + appropriate syntax is as follows. + + :param foo: the foo parameter + :param bar: the bar parameter + :returns: return_type -- description of the return value + :returns: description of the return value + :raises: AttributeError, KeyError + + """ + + +Dictionaries/Lists +------------------ +If a dictionary (dict) or list object is longer than 80 characters, its items +should be split with newlines. Embedded iterables should have their items +indented. Additionally, the last item in the dictionary should have a trailing +comma. This increases readability and simplifies future diffs. + +Example:: + + my_dictionary = { + "image": { + "name": "Just a Snapshot", + "size": 2749573, + "properties": { + "user_id": 12, + "arch": "x86_64", + }, + "things": [ + "thing_one", + "thing_two", + ], + "status": "ACTIVE", + }, + } + + +Calling Methods +--------------- +Calls to methods 80 characters or longer should format each argument with +newlines. This is not a requirement, but a guideline:: + + unnecessarily_long_function_name('string one', + 'string two', + kwarg1=constants.ACTIVE, + kwarg2=['a', 'b', 'c']) + + +Rather than constructing parameters inline, it is better to break things up:: + + list_of_strings = [ + 'what_a_long_string', + 'not as long', + ] + + dict_of_numbers = { + 'one': 1, + 'two': 2, + 'twenty four': 24, + } + + object_one.call_a_method('string three', + 'string four', + kwarg1=list_of_strings, + kwarg2=dict_of_numbers) + + +Internationalization (i18n) Strings +----------------------------------- +In order to support multiple languages, we have a mechanism to support +automatic translations of exception and log strings. + +Example:: + + msg = _("An error occurred") + raise HTTPBadRequest(explanation=msg) + +If you have a variable to place within the string, first internationalize the +template string then do the replacement. + +Example:: + + msg = _("Missing parameter: %s") % ("flavor",) + LOG.error(msg) + +If you have multiple variables to place in the string, use keyword parameters. +This helps our translators reorder parameters when needed. + +Example:: + + msg = _("The server with id %(s_id)s has no key %(m_key)s") + LOG.error(msg % {"s_id": "1234", "m_key": "imageId"}) + + +Creating Unit Tests +------------------- +For every new feature, unit tests should be created that both test and +(implicitly) document the usage of said feature. If submitting a patch for a +bug that had no unit test, a new passing unit test should be added. If a +submitted bug fix does have a unit test, be sure to add a new one that fails +without the patch and passes with the patch. + +For more information on creating unit tests and utilizing the testing +infrastructure in OpenStack Nova, please read nova/testing/README.rst. diff --git a/doc/source/code.rst b/doc/source/code.rst index 73fc31e1a..06549e1f5 100644 --- a/doc/source/code.rst +++ b/doc/source/code.rst @@ -29,7 +29,7 @@ Generating source/api/nova..db.sqlalchemy.api.rst Generating source/api/nova..db.sqlalchemy.models.rst Generating source/api/nova..db.sqlalchemy.session.rst Generating source/api/nova..exception.rst -Generating source/api/nova..fakerabbit.rst +Generating source/api/nova..fake.rabbit.rst Generating source/api/nova..flags.rst Generating source/api/nova..image.service.rst Generating source/api/nova..manager.rst diff --git a/doc/source/devref/fakes.rst b/doc/source/devref/fakes.rst index 6073447f0..0aa37ce45 100644 --- a/doc/source/devref/fakes.rst +++ b/doc/source/devref/fakes.rst @@ -44,10 +44,10 @@ The :mod:`nova.auth.fakeldap` Module :show-inheritance: -The :mod:`nova.fakerabbit` Module +The :mod:`nova.testing.fake.rabbit` Module --------------------------------- -.. automodule:: nova.fakerabbit +.. automodule:: nova.testing.fake.rabbit :noindex: :members: :undoc-members: diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 23042dab0..0433d5152 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -120,7 +120,7 @@ class Lockout(wsgi.Middleware): if FLAGS.memcached_servers: import memcache else: - from nova import fakememcache as memcache + from nova.testing.fake import memcache self.mc = memcache.Client(FLAGS.memcached_servers, debug=0) super(Lockout, self).__init__(application) diff --git a/nova/auth/ldapdriver.py b/nova/auth/ldapdriver.py index bc37d2d87..6b9969dad 100644 --- a/nova/auth/ldapdriver.py +++ b/nova/auth/ldapdriver.py @@ -72,7 +72,7 @@ LOG = logging.getLogger("nova.ldapdriver") if FLAGS.memcached_servers: import memcache else: - from nova import fakememcache as memcache + from nova.testing.fake import memcache # TODO(vish): make an abstract base class with the same public methods diff --git a/nova/auth/manager.py b/nova/auth/manager.py index 93b4244ad..3d984f1bf 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -82,7 +82,7 @@ LOG = logging.getLogger('nova.auth.manager') if FLAGS.memcached_servers: import memcache else: - from nova import fakememcache as memcache + from nova.testing.fake import memcache class AuthBase(object): diff --git a/nova/fakememcache.py b/nova/fakememcache.py deleted file mode 100644 index e4f238aa9..000000000 --- a/nova/fakememcache.py +++ /dev/null @@ -1,59 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# 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. - -"""Super simple fake memcache client.""" - -from nova import utils - - -class Client(object): - """Replicates a tiny subset of memcached client interface.""" - - def __init__(self, *args, **kwargs): - """Ignores the passed in args.""" - self.cache = {} - - def get(self, key): - """Retrieves the value for a key or None.""" - (timeout, value) = self.cache.get(key, (0, None)) - if timeout == 0 or utils.utcnow_ts() < timeout: - return value - return None - - def set(self, key, value, time=0, min_compress_len=0): - """Sets the value for a key.""" - timeout = 0 - if time != 0: - timeout = utils.utcnow_ts() + time - self.cache[key] = (timeout, value) - return True - - def add(self, key, value, time=0, min_compress_len=0): - """Sets the value for a key if it doesn't exist.""" - if not self.get(key) is None: - return False - return self.set(key, value, time, min_compress_len) - - def incr(self, key, delta=1): - """Increments the value for a key.""" - value = self.get(key) - if value is None: - return None - new_value = int(value) + delta - self.cache[key] = (self.cache[key][0], str(new_value)) - return new_value diff --git a/nova/fakerabbit.py b/nova/fakerabbit.py deleted file mode 100644 index 2807a76f6..000000000 --- a/nova/fakerabbit.py +++ /dev/null @@ -1,153 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# 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. - -"""Based a bit on the carrot.backends.queue backend... but a lot better.""" - -import Queue as queue - -from carrot.backends import base -from eventlet import greenthread - -from nova import log as logging - - -LOG = logging.getLogger("nova.fakerabbit") - - -EXCHANGES = {} -QUEUES = {} -CONSUMERS = {} - - -class Message(base.BaseMessage): - pass - - -class Exchange(object): - def __init__(self, name, exchange_type): - self.name = name - self.exchange_type = exchange_type - self._queue = queue.Queue() - self._routes = {} - - def publish(self, message, routing_key=None): - nm = self.name - LOG.debug(_('(%(nm)s) publish (key: %(routing_key)s)' - ' %(message)s') % locals()) - if routing_key in self._routes: - for f in self._routes[routing_key]: - LOG.debug(_('Publishing to route %s'), f) - f(message, routing_key=routing_key) - - def bind(self, callback, routing_key): - self._routes.setdefault(routing_key, []) - self._routes[routing_key].append(callback) - - -class Queue(object): - def __init__(self, name): - self.name = name - self._queue = queue.Queue() - - def __repr__(self): - return '' % self.name - - def push(self, message, routing_key=None): - self._queue.put(message) - - def size(self): - return self._queue.qsize() - - def pop(self): - return self._queue.get() - - -class Backend(base.BaseBackend): - def queue_declare(self, queue, **kwargs): - global QUEUES - if queue not in QUEUES: - LOG.debug(_('Declaring queue %s'), queue) - QUEUES[queue] = Queue(queue) - - def exchange_declare(self, exchange, type, *args, **kwargs): - global EXCHANGES - if exchange not in EXCHANGES: - LOG.debug(_('Declaring exchange %s'), exchange) - EXCHANGES[exchange] = Exchange(exchange, type) - - def queue_bind(self, queue, exchange, routing_key, **kwargs): - global EXCHANGES - global QUEUES - LOG.debug(_('Binding %(queue)s to %(exchange)s with' - ' key %(routing_key)s') % locals()) - EXCHANGES[exchange].bind(QUEUES[queue].push, routing_key) - - def declare_consumer(self, queue, callback, consumer_tag, *args, **kwargs): - global CONSUMERS - LOG.debug("Adding consumer %s", consumer_tag) - CONSUMERS[consumer_tag] = (queue, callback) - - def cancel(self, consumer_tag): - global CONSUMERS - LOG.debug("Removing consumer %s", consumer_tag) - del CONSUMERS[consumer_tag] - - def consume(self, limit=None): - global CONSUMERS - num = 0 - while True: - for (queue, callback) in CONSUMERS.itervalues(): - item = self.get(queue) - if item: - callback(item) - num += 1 - yield - if limit and num == limit: - raise StopIteration() - greenthread.sleep(0.1) - - def get(self, queue, no_ack=False): - global QUEUES - if not queue in QUEUES or not QUEUES[queue].size(): - return None - (message_data, content_type, content_encoding) = QUEUES[queue].pop() - message = Message(backend=self, body=message_data, - content_type=content_type, - content_encoding=content_encoding) - message.result = True - LOG.debug(_('Getting from %(queue)s: %(message)s') % locals()) - return message - - def prepare_message(self, message_data, delivery_mode, - content_type, content_encoding, **kwargs): - """Prepare message for sending.""" - return (message_data, content_type, content_encoding) - - def publish(self, message, exchange, routing_key, **kwargs): - global EXCHANGES - if exchange in EXCHANGES: - EXCHANGES[exchange].publish(message, routing_key=routing_key) - - -def reset_all(): - global EXCHANGES - global QUEUES - global CONSUMERS - EXCHANGES = {} - QUEUES = {} - CONSUMERS = {} diff --git a/nova/rpc/impl_carrot.py b/nova/rpc/impl_carrot.py index 57fd074f0..eed8cb10d 100644 --- a/nova/rpc/impl_carrot.py +++ b/nova/rpc/impl_carrot.py @@ -41,9 +41,9 @@ import greenlet from nova import context from nova import exception -from nova import fakerabbit from nova import flags from nova.rpc.common import RemoteError, LOG +from nova.testing import fake # Needed for tests eventlet.monkey_patch() @@ -71,7 +71,7 @@ class Connection(carrot_connection.BrokerConnection): virtual_host=FLAGS.rabbit_virtual_host) if FLAGS.fake_rabbit: - params['backend_cls'] = fakerabbit.Backend + params['backend_cls'] = fake.rabbit.Backend # NOTE(vish): magic is fun! # pylint: disable=W0142 diff --git a/nova/test.py b/nova/test.py index 6c565f53d..448af047e 100644 --- a/nova/test.py +++ b/nova/test.py @@ -35,12 +35,12 @@ import nova.image.fake import shutil import stubout -from nova import fakerabbit from nova import flags from nova import log from nova import rpc from nova import utils from nova import service +from nova.testing.fake import rabbit from nova.virt import fake @@ -142,7 +142,7 @@ class TestCase(unittest.TestCase): finally: # Clean out fake_rabbit's queue if we used it if FLAGS.fake_rabbit: - fakerabbit.reset_all() + rabbit.reset_all() if FLAGS.connection_type == 'fake': if hasattr(fake.FakeConnection, '_instance'): diff --git a/nova/testing/README.rst b/nova/testing/README.rst new file mode 100644 index 000000000..036f1c77d --- /dev/null +++ b/nova/testing/README.rst @@ -0,0 +1,44 @@ +===================================== +OpenStack Nova Testing Infrastructure +===================================== + +A note of clarification is in order, to help those who are new to testing in +OpenStack nova: + +- actual unit tests are created in the "tests" directory; +- the "testing" directory is used to house the infrastructure needed to support + testing in OpenStack Nova. + +This README file attempts to provide current and prospective contributors with +everything they need to know in order to start creating unit tests and +utilizing the convenience code provided in nova.testing. + +Note: the content for the rest of this file will be added as the work items in +the following blueprint are completed: + https://blueprints.launchpad.net/nova/+spec/consolidate-testing-infrastructure + + +Test Types: Unit vs. Functional vs. Integration +----------------------------------------------- + +TBD + +Writing Unit Tests +------------------ + +TBD + +Using Fakes +~~~~~~~~~~~ + +TBD + +Writing Functional Tests +------------------------ + +TBD + +Writing Integration Tests +------------------------- + +TBD diff --git a/nova/testing/__init__.py b/nova/testing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/nova/testing/fake/__init__.py b/nova/testing/fake/__init__.py new file mode 100644 index 000000000..d9eebbd23 --- /dev/null +++ b/nova/testing/fake/__init__.py @@ -0,0 +1,2 @@ +import memcache +import rabbit diff --git a/nova/testing/fake/memcache.py b/nova/testing/fake/memcache.py new file mode 100644 index 000000000..e4f238aa9 --- /dev/null +++ b/nova/testing/fake/memcache.py @@ -0,0 +1,59 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# 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. + +"""Super simple fake memcache client.""" + +from nova import utils + + +class Client(object): + """Replicates a tiny subset of memcached client interface.""" + + def __init__(self, *args, **kwargs): + """Ignores the passed in args.""" + self.cache = {} + + def get(self, key): + """Retrieves the value for a key or None.""" + (timeout, value) = self.cache.get(key, (0, None)) + if timeout == 0 or utils.utcnow_ts() < timeout: + return value + return None + + def set(self, key, value, time=0, min_compress_len=0): + """Sets the value for a key.""" + timeout = 0 + if time != 0: + timeout = utils.utcnow_ts() + time + self.cache[key] = (timeout, value) + return True + + def add(self, key, value, time=0, min_compress_len=0): + """Sets the value for a key if it doesn't exist.""" + if not self.get(key) is None: + return False + return self.set(key, value, time, min_compress_len) + + def incr(self, key, delta=1): + """Increments the value for a key.""" + value = self.get(key) + if value is None: + return None + new_value = int(value) + delta + self.cache[key] = (self.cache[key][0], str(new_value)) + return new_value diff --git a/nova/testing/fake/rabbit.py b/nova/testing/fake/rabbit.py new file mode 100644 index 000000000..0cb91bd25 --- /dev/null +++ b/nova/testing/fake/rabbit.py @@ -0,0 +1,153 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# 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. + +"""Based a bit on the carrot.backends.queue backend... but a lot better.""" + +import Queue as queue + +from carrot.backends import base +from eventlet import greenthread + +from nova import log as logging + + +LOG = logging.getLogger("nova.testing.fake.rabbit") + + +EXCHANGES = {} +QUEUES = {} +CONSUMERS = {} + + +class Message(base.BaseMessage): + pass + + +class Exchange(object): + def __init__(self, name, exchange_type): + self.name = name + self.exchange_type = exchange_type + self._queue = queue.Queue() + self._routes = {} + + def publish(self, message, routing_key=None): + nm = self.name + LOG.debug(_('(%(nm)s) publish (key: %(routing_key)s)' + ' %(message)s') % locals()) + if routing_key in self._routes: + for f in self._routes[routing_key]: + LOG.debug(_('Publishing to route %s'), f) + f(message, routing_key=routing_key) + + def bind(self, callback, routing_key): + self._routes.setdefault(routing_key, []) + self._routes[routing_key].append(callback) + + +class Queue(object): + def __init__(self, name): + self.name = name + self._queue = queue.Queue() + + def __repr__(self): + return '' % self.name + + def push(self, message, routing_key=None): + self._queue.put(message) + + def size(self): + return self._queue.qsize() + + def pop(self): + return self._queue.get() + + +class Backend(base.BaseBackend): + def queue_declare(self, queue, **kwargs): + global QUEUES + if queue not in QUEUES: + LOG.debug(_('Declaring queue %s'), queue) + QUEUES[queue] = Queue(queue) + + def exchange_declare(self, exchange, type, *args, **kwargs): + global EXCHANGES + if exchange not in EXCHANGES: + LOG.debug(_('Declaring exchange %s'), exchange) + EXCHANGES[exchange] = Exchange(exchange, type) + + def queue_bind(self, queue, exchange, routing_key, **kwargs): + global EXCHANGES + global QUEUES + LOG.debug(_('Binding %(queue)s to %(exchange)s with' + ' key %(routing_key)s') % locals()) + EXCHANGES[exchange].bind(QUEUES[queue].push, routing_key) + + def declare_consumer(self, queue, callback, consumer_tag, *args, **kwargs): + global CONSUMERS + LOG.debug("Adding consumer %s", consumer_tag) + CONSUMERS[consumer_tag] = (queue, callback) + + def cancel(self, consumer_tag): + global CONSUMERS + LOG.debug("Removing consumer %s", consumer_tag) + del CONSUMERS[consumer_tag] + + def consume(self, limit=None): + global CONSUMERS + num = 0 + while True: + for (queue, callback) in CONSUMERS.itervalues(): + item = self.get(queue) + if item: + callback(item) + num += 1 + yield + if limit and num == limit: + raise StopIteration() + greenthread.sleep(0.1) + + def get(self, queue, no_ack=False): + global QUEUES + if not queue in QUEUES or not QUEUES[queue].size(): + return None + (message_data, content_type, content_encoding) = QUEUES[queue].pop() + message = Message(backend=self, body=message_data, + content_type=content_type, + content_encoding=content_encoding) + message.result = True + LOG.debug(_('Getting from %(queue)s: %(message)s') % locals()) + return message + + def prepare_message(self, message_data, delivery_mode, + content_type, content_encoding, **kwargs): + """Prepare message for sending.""" + return (message_data, content_type, content_encoding) + + def publish(self, message, exchange, routing_key, **kwargs): + global EXCHANGES + if exchange in EXCHANGES: + EXCHANGES[exchange].publish(message, routing_key=routing_key) + + +def reset_all(): + global EXCHANGES + global QUEUES + global CONSUMERS + EXCHANGES = {} + QUEUES = {} + CONSUMERS = {} diff --git a/nova/testing/runner.py b/nova/testing/runner.py new file mode 100644 index 000000000..9fbbb418d --- /dev/null +++ b/nova/testing/runner.py @@ -0,0 +1,370 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# 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. + +# Colorizer Code is borrowed from Twisted: +# Copyright (c) 2001-2010 Twisted Matrix Laboratories. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Unittest runner for Nova. + +To run all tests + python nova/testing/runner.py + +To run a single test module: + python nova/testing/runner.py test_compute + + or + + python nova/testing/runner.py api.test_wsgi + +To run a single test: + python nova/testing/runner.py \ + test_compute:ComputeTestCase.test_run_terminate + +""" + +import gettext +import heapq +import os +import unittest +import sys +import time + +gettext.install('nova', unicode=1) + +import eventlet +from nose import config +from nose import core +from nose import result + +from nova import log as logging + + +class _AnsiColorizer(object): + """ + A colorizer is an object that loosely wraps around a stream, allowing + callers to write text to the stream in a particular color. + + Colorizer classes must implement C{supported()} and C{write(text, color)}. + """ + _colors = dict(black=30, red=31, green=32, yellow=33, + blue=34, magenta=35, cyan=36, white=37) + + def __init__(self, stream): + self.stream = stream + + def supported(cls, stream=sys.stdout): + """ + A class method that returns True if the current platform supports + coloring terminal output using this method. Returns False otherwise. + """ + if not stream.isatty(): + return False # auto color only on TTYs + try: + import curses + except ImportError: + return False + else: + try: + try: + return curses.tigetnum("colors") > 2 + except curses.error: + curses.setupterm() + return curses.tigetnum("colors") > 2 + except: + raise + # guess false in case of error + return False + supported = classmethod(supported) + + def write(self, text, color): + """ + Write the given text to the stream in the given color. + + @param text: Text to be written to the stream. + + @param color: A string label for a color. e.g. 'red', 'white'. + """ + color = self._colors[color] + self.stream.write('\x1b[%s;1m%s\x1b[0m' % (color, text)) + + +class _Win32Colorizer(object): + """ + See _AnsiColorizer docstring. + """ + def __init__(self, stream): + from win32console import GetStdHandle, STD_OUT_HANDLE, \ + FOREGROUND_RED, FOREGROUND_BLUE, FOREGROUND_GREEN, \ + FOREGROUND_INTENSITY + red, green, blue, bold = (FOREGROUND_RED, FOREGROUND_GREEN, + FOREGROUND_BLUE, FOREGROUND_INTENSITY) + self.stream = stream + self.screenBuffer = GetStdHandle(STD_OUT_HANDLE) + self._colors = { + 'normal': red | green | blue, + 'red': red | bold, + 'green': green | bold, + 'blue': blue | bold, + 'yellow': red | green | bold, + 'magenta': red | blue | bold, + 'cyan': green | blue | bold, + 'white': red | green | blue | bold + } + + def supported(cls, stream=sys.stdout): + try: + import win32console + screenBuffer = win32console.GetStdHandle( + win32console.STD_OUT_HANDLE) + except ImportError: + return False + import pywintypes + try: + screenBuffer.SetConsoleTextAttribute( + win32console.FOREGROUND_RED | + win32console.FOREGROUND_GREEN | + win32console.FOREGROUND_BLUE) + except pywintypes.error: + return False + else: + return True + supported = classmethod(supported) + + def write(self, text, color): + color = self._colors[color] + self.screenBuffer.SetConsoleTextAttribute(color) + self.stream.write(text) + self.screenBuffer.SetConsoleTextAttribute(self._colors['normal']) + + +class _NullColorizer(object): + """ + See _AnsiColorizer docstring. + """ + def __init__(self, stream): + self.stream = stream + + def supported(cls, stream=sys.stdout): + return True + supported = classmethod(supported) + + def write(self, text, color): + self.stream.write(text) + + +def get_elapsed_time_color(elapsed_time): + if elapsed_time > 1.0: + return 'red' + elif elapsed_time > 0.25: + return 'yellow' + else: + return 'green' + + +class NovaTestResult(result.TextTestResult): + def __init__(self, *args, **kw): + self.show_elapsed = kw.pop('show_elapsed') + result.TextTestResult.__init__(self, *args, **kw) + self.num_slow_tests = 5 + self.slow_tests = [] # this is a fixed-sized heap + self._last_case = None + self.colorizer = None + # NOTE(vish): reset stdout for the terminal check + stdout = sys.stdout + sys.stdout = sys.__stdout__ + for colorizer in [_Win32Colorizer, _AnsiColorizer, _NullColorizer]: + if colorizer.supported(): + self.colorizer = colorizer(self.stream) + break + sys.stdout = stdout + + # NOTE(lorinh): Initialize start_time in case a sqlalchemy-migrate + # error results in it failing to be initialized later. Otherwise, + # _handleElapsedTime will fail, causing the wrong error message to + # be outputted. + self.start_time = time.time() + + def getDescription(self, test): + return str(test) + + def _handleElapsedTime(self, test): + self.elapsed_time = time.time() - self.start_time + item = (self.elapsed_time, test) + # Record only the n-slowest tests using heap + if len(self.slow_tests) >= self.num_slow_tests: + heapq.heappushpop(self.slow_tests, item) + else: + heapq.heappush(self.slow_tests, item) + + def _writeElapsedTime(self, test): + color = get_elapsed_time_color(self.elapsed_time) + self.colorizer.write(" %.2f" % self.elapsed_time, color) + + def _writeResult(self, test, long_result, color, short_result, success): + if self.showAll: + self.colorizer.write(long_result, color) + if self.show_elapsed and success: + self._writeElapsedTime(test) + self.stream.writeln() + elif self.dots: + self.stream.write(short_result) + self.stream.flush() + + # NOTE(vish): copied from unittest with edit to add color + def addSuccess(self, test): + unittest.TestResult.addSuccess(self, test) + self._handleElapsedTime(test) + self._writeResult(test, 'OK', 'green', '.', True) + + # NOTE(vish): copied from unittest with edit to add color + def addFailure(self, test, err): + unittest.TestResult.addFailure(self, test, err) + self._handleElapsedTime(test) + self._writeResult(test, 'FAIL', 'red', 'F', False) + + # NOTE(vish): copied from nose with edit to add color + def addError(self, test, err): + """Overrides normal addError to add support for + errorClasses. If the exception is a registered class, the + error will be added to the list for that class, not errors. + """ + self._handleElapsedTime(test) + stream = getattr(self, 'stream', None) + ec, ev, tb = err + try: + exc_info = self._exc_info_to_string(err, test) + except TypeError: + # 2.3 compat + exc_info = self._exc_info_to_string(err) + for cls, (storage, label, isfail) in self.errorClasses.items(): + if result.isclass(ec) and issubclass(ec, cls): + if isfail: + test.passed = False + storage.append((test, exc_info)) + # Might get patched into a streamless result + if stream is not None: + if self.showAll: + message = [label] + detail = result._exception_detail(err[1]) + if detail: + message.append(detail) + stream.writeln(": ".join(message)) + elif self.dots: + stream.write(label[:1]) + return + self.errors.append((test, exc_info)) + test.passed = False + if stream is not None: + self._writeResult(test, 'ERROR', 'red', 'E', False) + + def startTest(self, test): + unittest.TestResult.startTest(self, test) + self.start_time = time.time() + current_case = test.test.__class__.__name__ + + if self.showAll: + if current_case != self._last_case: + self.stream.writeln(current_case) + self._last_case = current_case + + self.stream.write( + ' %s' % str(test.test._testMethodName).ljust(60)) + self.stream.flush() + + +class NovaTestRunner(core.TextTestRunner): + def __init__(self, *args, **kwargs): + self.show_elapsed = kwargs.pop('show_elapsed') + core.TextTestRunner.__init__(self, *args, **kwargs) + + def _makeResult(self): + return NovaTestResult(self.stream, + self.descriptions, + self.verbosity, + self.config, + show_elapsed=self.show_elapsed) + + def _writeSlowTests(self, result_): + # Pare out 'fast' tests + slow_tests = [item for item in result_.slow_tests + if get_elapsed_time_color(item[0]) != 'green'] + if slow_tests: + slow_total_time = sum(item[0] for item in slow_tests) + self.stream.writeln("Slowest %i tests took %.2f secs:" + % (len(slow_tests), slow_total_time)) + for elapsed_time, test in sorted(slow_tests, reverse=True): + time_str = "%.2f" % elapsed_time + self.stream.writeln(" %s %s" % (time_str.ljust(10), test)) + + def run(self, test): + result_ = core.TextTestRunner.run(self, test) + if self.show_elapsed: + self._writeSlowTests(result_) + return result_ + + +def run(): + logging.setup() + # If any argument looks like a test name but doesn't have "nova.tests" in + # front of it, automatically add that so we don't have to type as much + show_elapsed = True + argv = [] + for x in sys.argv: + if x.startswith('test_'): + argv.append('nova.tests.%s' % x) + elif x.startswith('--hide-elapsed'): + show_elapsed = False + else: + argv.append(x) + + testdir = os.path.abspath(os.path.join("nova", "tests")) + c = config.Config(stream=sys.stdout, + env=os.environ, + verbosity=3, + workingDir=testdir, + plugins=core.DefaultPluginManager()) + + runner = NovaTestRunner(stream=c.stream, + verbosity=c.verbosity, + config=c, + show_elapsed=show_elapsed) + sys.exit(not core.run(config=c, testRunner=runner, argv=argv)) + + +if __name__ == '__main__': + eventlet.monkey_patch() + run() diff --git a/nova/tests/test_iptables_network.py b/nova/tests/test_iptables_network.py index 918034269..478dfa70d 100644 --- a/nova/tests/test_iptables_network.py +++ b/nova/tests/test_iptables_network.py @@ -84,12 +84,12 @@ class IptablesManagerTestCase(test.TestCase): table = self.manager.ipv4['filter'] table.add_rule('FORWARD', '-s 1.2.3.4/5 -j DROP') new_lines = self.manager._modify_rules(current_lines, table) - self.assertTrue('-A run_tests.py-FORWARD ' + self.assertTrue('-A runner.py-FORWARD ' '-s 1.2.3.4/5 -j DROP' in new_lines) table.remove_rule('FORWARD', '-s 1.2.3.4/5 -j DROP') new_lines = self.manager._modify_rules(current_lines, table) - self.assertTrue('-A run_tests.py-FORWARD ' + self.assertTrue('-A runner.py-FORWARD ' '-s 1.2.3.4/5 -j DROP' not in new_lines) def test_nat_rules(self): @@ -123,7 +123,7 @@ class IptablesManagerTestCase(test.TestCase): "nova-postouting-bottom: %s" % last_postrouting_line) for chain in ['POSTROUTING', 'PREROUTING', 'OUTPUT']: - self.assertTrue('-A %s -j run_tests.py-%s' \ + self.assertTrue('-A %s -j runner.py-%s' \ % (chain, chain) in new_lines, "Built-in chain %s not wrapped" % (chain,)) @@ -155,10 +155,10 @@ class IptablesManagerTestCase(test.TestCase): break self.assertTrue('-A nova-filter-top ' - '-j run_tests.py-local' in new_lines, + '-j runner.py-local' in new_lines, "nova-filter-top does not jump to wrapped local chain") for chain in ['INPUT', 'OUTPUT', 'FORWARD']: - self.assertTrue('-A %s -j run_tests.py-%s' \ + self.assertTrue('-A %s -j runner.py-%s' \ % (chain, chain) in new_lines, "Built-in chain %s not wrapped" % (chain,)) diff --git a/run_tests.py b/run_tests.py deleted file mode 100644 index 17547b8e0..000000000 --- a/run_tests.py +++ /dev/null @@ -1,365 +0,0 @@ -#!/usr/bin/env python -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# 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. - -# Colorizer Code is borrowed from Twisted: -# Copyright (c) 2001-2010 Twisted Matrix Laboratories. -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Unittest runner for Nova. - -To run all tests - python run_tests.py - -To run a single test: - python run_tests.py test_compute:ComputeTestCase.test_run_terminate - -To run a single test module: - python run_tests.py test_compute - - or - - python run_tests.py api.test_wsgi - -""" - -import gettext -import heapq -import os -import unittest -import sys -import time - -gettext.install('nova', unicode=1) - -import eventlet -from nose import config -from nose import core -from nose import result - -from nova import log as logging - - -class _AnsiColorizer(object): - """ - A colorizer is an object that loosely wraps around a stream, allowing - callers to write text to the stream in a particular color. - - Colorizer classes must implement C{supported()} and C{write(text, color)}. - """ - _colors = dict(black=30, red=31, green=32, yellow=33, - blue=34, magenta=35, cyan=36, white=37) - - def __init__(self, stream): - self.stream = stream - - def supported(cls, stream=sys.stdout): - """ - A class method that returns True if the current platform supports - coloring terminal output using this method. Returns False otherwise. - """ - if not stream.isatty(): - return False # auto color only on TTYs - try: - import curses - except ImportError: - return False - else: - try: - try: - return curses.tigetnum("colors") > 2 - except curses.error: - curses.setupterm() - return curses.tigetnum("colors") > 2 - except: - raise - # guess false in case of error - return False - supported = classmethod(supported) - - def write(self, text, color): - """ - Write the given text to the stream in the given color. - - @param text: Text to be written to the stream. - - @param color: A string label for a color. e.g. 'red', 'white'. - """ - color = self._colors[color] - self.stream.write('\x1b[%s;1m%s\x1b[0m' % (color, text)) - - -class _Win32Colorizer(object): - """ - See _AnsiColorizer docstring. - """ - def __init__(self, stream): - from win32console import GetStdHandle, STD_OUT_HANDLE, \ - FOREGROUND_RED, FOREGROUND_BLUE, FOREGROUND_GREEN, \ - FOREGROUND_INTENSITY - red, green, blue, bold = (FOREGROUND_RED, FOREGROUND_GREEN, - FOREGROUND_BLUE, FOREGROUND_INTENSITY) - self.stream = stream - self.screenBuffer = GetStdHandle(STD_OUT_HANDLE) - self._colors = { - 'normal': red | green | blue, - 'red': red | bold, - 'green': green | bold, - 'blue': blue | bold, - 'yellow': red | green | bold, - 'magenta': red | blue | bold, - 'cyan': green | blue | bold, - 'white': red | green | blue | bold - } - - def supported(cls, stream=sys.stdout): - try: - import win32console - screenBuffer = win32console.GetStdHandle( - win32console.STD_OUT_HANDLE) - except ImportError: - return False - import pywintypes - try: - screenBuffer.SetConsoleTextAttribute( - win32console.FOREGROUND_RED | - win32console.FOREGROUND_GREEN | - win32console.FOREGROUND_BLUE) - except pywintypes.error: - return False - else: - return True - supported = classmethod(supported) - - def write(self, text, color): - color = self._colors[color] - self.screenBuffer.SetConsoleTextAttribute(color) - self.stream.write(text) - self.screenBuffer.SetConsoleTextAttribute(self._colors['normal']) - - -class _NullColorizer(object): - """ - See _AnsiColorizer docstring. - """ - def __init__(self, stream): - self.stream = stream - - def supported(cls, stream=sys.stdout): - return True - supported = classmethod(supported) - - def write(self, text, color): - self.stream.write(text) - - -def get_elapsed_time_color(elapsed_time): - if elapsed_time > 1.0: - return 'red' - elif elapsed_time > 0.25: - return 'yellow' - else: - return 'green' - - -class NovaTestResult(result.TextTestResult): - def __init__(self, *args, **kw): - self.show_elapsed = kw.pop('show_elapsed') - result.TextTestResult.__init__(self, *args, **kw) - self.num_slow_tests = 5 - self.slow_tests = [] # this is a fixed-sized heap - self._last_case = None - self.colorizer = None - # NOTE(vish): reset stdout for the terminal check - stdout = sys.stdout - sys.stdout = sys.__stdout__ - for colorizer in [_Win32Colorizer, _AnsiColorizer, _NullColorizer]: - if colorizer.supported(): - self.colorizer = colorizer(self.stream) - break - sys.stdout = stdout - - # NOTE(lorinh): Initialize start_time in case a sqlalchemy-migrate - # error results in it failing to be initialized later. Otherwise, - # _handleElapsedTime will fail, causing the wrong error message to - # be outputted. - self.start_time = time.time() - - def getDescription(self, test): - return str(test) - - def _handleElapsedTime(self, test): - self.elapsed_time = time.time() - self.start_time - item = (self.elapsed_time, test) - # Record only the n-slowest tests using heap - if len(self.slow_tests) >= self.num_slow_tests: - heapq.heappushpop(self.slow_tests, item) - else: - heapq.heappush(self.slow_tests, item) - - def _writeElapsedTime(self, test): - color = get_elapsed_time_color(self.elapsed_time) - self.colorizer.write(" %.2f" % self.elapsed_time, color) - - def _writeResult(self, test, long_result, color, short_result, success): - if self.showAll: - self.colorizer.write(long_result, color) - if self.show_elapsed and success: - self._writeElapsedTime(test) - self.stream.writeln() - elif self.dots: - self.stream.write(short_result) - self.stream.flush() - - # NOTE(vish): copied from unittest with edit to add color - def addSuccess(self, test): - unittest.TestResult.addSuccess(self, test) - self._handleElapsedTime(test) - self._writeResult(test, 'OK', 'green', '.', True) - - # NOTE(vish): copied from unittest with edit to add color - def addFailure(self, test, err): - unittest.TestResult.addFailure(self, test, err) - self._handleElapsedTime(test) - self._writeResult(test, 'FAIL', 'red', 'F', False) - - # NOTE(vish): copied from nose with edit to add color - def addError(self, test, err): - """Overrides normal addError to add support for - errorClasses. If the exception is a registered class, the - error will be added to the list for that class, not errors. - """ - self._handleElapsedTime(test) - stream = getattr(self, 'stream', None) - ec, ev, tb = err - try: - exc_info = self._exc_info_to_string(err, test) - except TypeError: - # 2.3 compat - exc_info = self._exc_info_to_string(err) - for cls, (storage, label, isfail) in self.errorClasses.items(): - if result.isclass(ec) and issubclass(ec, cls): - if isfail: - test.passed = False - storage.append((test, exc_info)) - # Might get patched into a streamless result - if stream is not None: - if self.showAll: - message = [label] - detail = result._exception_detail(err[1]) - if detail: - message.append(detail) - stream.writeln(": ".join(message)) - elif self.dots: - stream.write(label[:1]) - return - self.errors.append((test, exc_info)) - test.passed = False - if stream is not None: - self._writeResult(test, 'ERROR', 'red', 'E', False) - - def startTest(self, test): - unittest.TestResult.startTest(self, test) - self.start_time = time.time() - current_case = test.test.__class__.__name__ - - if self.showAll: - if current_case != self._last_case: - self.stream.writeln(current_case) - self._last_case = current_case - - self.stream.write( - ' %s' % str(test.test._testMethodName).ljust(60)) - self.stream.flush() - - -class NovaTestRunner(core.TextTestRunner): - def __init__(self, *args, **kwargs): - self.show_elapsed = kwargs.pop('show_elapsed') - core.TextTestRunner.__init__(self, *args, **kwargs) - - def _makeResult(self): - return NovaTestResult(self.stream, - self.descriptions, - self.verbosity, - self.config, - show_elapsed=self.show_elapsed) - - def _writeSlowTests(self, result_): - # Pare out 'fast' tests - slow_tests = [item for item in result_.slow_tests - if get_elapsed_time_color(item[0]) != 'green'] - if slow_tests: - slow_total_time = sum(item[0] for item in slow_tests) - self.stream.writeln("Slowest %i tests took %.2f secs:" - % (len(slow_tests), slow_total_time)) - for elapsed_time, test in sorted(slow_tests, reverse=True): - time_str = "%.2f" % elapsed_time - self.stream.writeln(" %s %s" % (time_str.ljust(10), test)) - - def run(self, test): - result_ = core.TextTestRunner.run(self, test) - if self.show_elapsed: - self._writeSlowTests(result_) - return result_ - - -if __name__ == '__main__': - eventlet.monkey_patch() - logging.setup() - # If any argument looks like a test name but doesn't have "nova.tests" in - # front of it, automatically add that so we don't have to type as much - show_elapsed = True - argv = [] - for x in sys.argv: - if x.startswith('test_'): - argv.append('nova.tests.%s' % x) - elif x.startswith('--hide-elapsed'): - show_elapsed = False - else: - argv.append(x) - - testdir = os.path.abspath(os.path.join("nova", "tests")) - c = config.Config(stream=sys.stdout, - env=os.environ, - verbosity=3, - workingDir=testdir, - plugins=core.DefaultPluginManager()) - - runner = NovaTestRunner(stream=c.stream, - verbosity=c.verbosity, - config=c, - show_elapsed=show_elapsed) - sys.exit(not core.run(config=c, testRunner=runner, argv=argv)) diff --git a/run_tests.sh b/run_tests.sh index 837f9695a..a9b9dd1f9 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -117,7 +117,7 @@ function run_pep8 { --exclude=vcsversion.py ${srcfiles} } -NOSETESTS="python run_tests.py $noseopts $noseargs" +NOSETESTS="python nova/testing/runner.py $noseopts $noseargs" if [ $never_venv -eq 0 ] then -- cgit