Documentation for pulsar 0.4.6. For development docs, go here.
'''\
An asynchronous parallel testing suite :class:`pulsar.Application`.
Used for testing pulsar itself but it can be used as a test suite for
any other library.
Requirements
====================
* unittest2_ for python 2.6
* mock_ for python < 3.3
.. _apps-test-intro:
Introduction
====================
Create a script on the top level directory of your library,
let's call it ``runtests.py``::
from pulsar.apps import TestSuite
if __name__ == '__main__':
TestSuite(description='Test suite for my library',
modules=('regression',
('examples','tests'))).start()
where ``modules`` is an iterable for discovering test cases. Check the
:class:`TestLoader` for details.
In the above example the test suite will look for all python files
in the ``regression`` module (in a recursive fashion), and for modules
called ``tests`` in the ``example`` module.
.. _apps-test-loading:
Loading Tests
=================
Loading test cases is accomplished via the :class:`TestLoader` class. In
this context we refer to an ``object`` as a ``module`` (including a
directory module) or a ``class``.
These are the rules for loading tests:
* Directories that aren't packages are not inspected.
* Any class that is a ``unittest.TestCase`` subclass is collected.
* if an object starts with ``_`` or ``.`` it won't be collected,
nor will any objects it contains.
* If an object defines a ``__test__`` attribute that does not evaluate to True,
that object will not be collected, nor will any objects it contains.
Test Case
=============
Only subclasses of ``unittest.TestCase`` are collected by this application.
When running a test, pulsar looks for two extra method: ``_pre_setup`` and
``_post_teardown``. If the former is available, it is run just before the
``setUp`` method while if the latter is available, it is run
just after the ``tearDown`` method.
.. _unittest2: http://pypi.python.org/pypi/unittest2
.. _mock: http://pypi.python.org/pypi/mock
'''
__test__ = False
import logging
import os
import sys
import time
import inspect
if sys.version_info >= (2,7):
import unittest
else: # pragma nocover
try:
import unittest2 as unittest
except ImportError:
print('To run tests in python 2.6 you need to install\
the unittest2 package')
exit(0)
if sys.version_info < (3,3): # pragma nocover
try:
import mock
except ImportError:
print('To run tests you need to install the mock package')
exit(0)
else:
from unittest import mock
import pulsar
from pulsar.apps import tasks
from pulsar.utils import events
from .result import *
from .case import *
from .plugins.base import *
from .loader import *
from .utils import *
from .wsgi import *
def dont_run_with_thread(obj):
c = pulsar.get_actor().cfg.concurrency
d = unittest.skipUnless(c=='process',
'Run only when concurrency is process')
return d(obj)
class ExitTest(Exception):
pass
class TestVerbosity(TestOption):
name = 'verbosity'
flags = ['--verbosity']
type = int
default = 1
desc = """Test verbosity, 0, 1, 2, 3"""
class TestLabels(TestOption):
name = "labels"
nargs = '*'
validator = pulsar.validate_list
desc = """Optional test labels to run. If not provided\
all tests are run.
To see available labels use the -l option."""
class TestSize(TestOption):
name = 'size'
flags = ['--size']
#choices = ('tiny','small','normal','big','huge')
default = 'normal'
desc = """Optional test size."""
class TestList(TestOption):
name = "list_labels"
flags = ['-l','--list_labels']
action = 'store_true'
default = False
validator = pulsar.validate_bool
desc = """List all test labels without performing tests."""
test_commands = set()
@pulsar.command(internal=True, ack=False, commands_set=test_commands)
def test_result(client, actor, sender, tag, clsname, result):
'''Command for sending test results from test workers to the test monitor.'''
actor.logger.debug('Got a test results from %s.%s', tag, clsname)
return actor.app.add_result(actor, result)
[docs]class TestSuite(tasks.CPUboundServer):
'''An asynchronous test suite which works like a task queue where each task
is a group of tests specified in a test class.
:parameter modules: An iterable over modules where to look for tests.
Alternatively it can be a callable returning the iterable over modules.
For example::
suite = TestSuite(modules=('regression',
('examples','tests'),
('apps','test_*')))
def get_modules(suite):
...
suite = TestSuite(modules=get_modules)
If not provided it is set as default to ``["tests"]`` which loads all
python module from the tests module in a recursive fashion.
Check the the :class:`TestLoader` for detailed information.
:parameter result_class: Optional class for collecting test results. By default
it used the standard ``unittest.TextTestResult``.
:parameter plugins: Optional list of :class:`TestPlugin` instances.
'''
_app_name = 'test'
cfg_apps = ('cpubound',)
commands_set = test_commands
cfg = {'loglevel': 'none',
'timeout': 3600,
'backlog': 1,
'logconfig': {
'loggers': {
LOGGER.name: {'handlers': ['console_message'],
'level': logging.INFO}
}
}
}
def handler(self):
return self
def python_path(self):
#Override the python path so that we put the directory where the script
#is in the ppython path
path = os.getcwd()
if path not in sys.path:
sys.path.insert(0, path)
def on_config_init(self, cfg, params):
self.plugins = params.get('plugins') or ()
if self.plugins:
for plugin in self.plugins:
cfg.settings.update(plugin.config.settings)
@property
[docs] def runner(self):
'''Instance of :class:`TestRunner` driving the test case
configuration and plugins.'''
if 'runner' not in self.local:
result_class = getattr(self, 'result_class', None)
r = unittest.TextTestRunner()
stream = r.stream
runner = TestRunner(self.plugins, stream, result_class)
abort_message = runner.configure(self.cfg)
if abort_message:
raise ExitTest(str(abort_message))
self.local.runner = runner
return self.local.runner
def on_config(self):
#When config is available load the tests and check what type of
#action is required.
modules = getattr(self, 'modules', None)
if not hasattr(self, 'plugins'):
self.plugins = ()
# Create a runner and configure it
runner = self.runner
if not modules:
modules = ['tests']
if hasattr(modules, '__call__'):
modules = modules(self)
loader = TestLoader(os.getcwd(), modules, runner, logger=self.logger)
# Listing labels
if self.cfg.list_labels:
tags = self.cfg.labels
if tags:
s = '' if len(tags) == 1 else 's'
print('\nTest labels for label{0} {1}\n'\
.format(s,', '.join(tags)))
else:
print('\nAll test labels\n')
def _tags():
for tag, mod in loader.testmodules(tags):
doc = mod.__doc__
if doc:
tag = '{0} - {1}'.format(tag,doc)
yield tag
for tag in sorted(_tags()):
print(tag)
print('\n')
return False
self.local.loader = loader
def monitor_start(self, monitor):
# When the monitor starts load all :class:`TestRequest` into the
# in the :attr:`pulsar.Actor.ioqueue`.
loader = self.local.loader
tags = self.cfg.labels
try:
self.local.tests = tests = list(loader.testclasses(tags))
if tests:
self.logger.info('loaded %s test classes', len(tests))
self.runner.on_start()
events.fire('tests', self, tests=tests)
monitor.cfg.set('workers', min(self.cfg.workers, len(tests)))
self._time_start = None
else:
raise ExitTest('Could not find any tests.')
except ExitTest as e:
print(str(e))
monitor.arbiter.stop()
except Exception:
LOGGER.critical('Error occurred before starting tests',
exc_info=True)
monitor.arbiter.stop()
def monitor_task(self, monitor):
if self._time_start is None:
tests = self.local.tests
self.logger.info('sending %s test classes to the task queue',
len(tests))
self._time_start = time.time()
for tag, testcls in tests:
monitor.put(TestRequest(testcls, tag))
def add_result(self, monitor, result):
#Check if we got all results
runner = self.runner
runner.add(result)
if runner.count == len(self.local.tests):
time_taken = time.time() - self._time_start
runner.on_end()
runner.printSummary(time_taken)
# Shut down the arbiter
if runner.result.errors or runner.result.failures:
exit_code = 1
else:
exit_code = 0
return monitor.arbiter.stop(exit_code=exit_code)