diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9424025 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: python +env: + - TOX_ENV=py26 + - TOX_ENV=py27 + - TOX_ENV=pypy + - TOX_ENV=nose-1-0 + - TOX_ENV=nose-1-3 +# commands to install dependencies +install: + - pip install tox --use-mirrors +# commands to run +script: + - tox -e $TOX_ENV diff --git a/AUTHORS b/AUTHORS index 2dc5869..8bc838e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1 +1,2 @@ Wes Winham +Jeff Meadows diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f02880..137ae92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ Changelog ========= +## 0.2.0 + +Test split granularity can now be controlled by setting _distributed_can_split_ +at the class or module level. Simply specify `_distributed_can_split = False` on +a module or on a class, and tests contained therein will be forced to run on the +same node. + ## 0.1.2 Test selection for Class-based tests no longer groups all methods from the same diff --git a/README.md b/README.md index 5266bad..62cab2c 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,18 @@ Alternatively, you can use the environment variables: * `NOSE_NODES` * `NOSE_NODE_NUMBER` +### Specifying how tests are split + +By default, each test function or method can be run on a different machine. This, +however, is not always the best way to split tests; sometimes it's preferable to +keep tests in a certain class or module on the same machine. + +Simply specify + + _distributed_can_split_ = False + +in the class or module for which the containing tests should not be split. + ### Temporarily disabling test distribution In the case that you're using environment variables diff --git a/distributed_nose/__init__.py b/distributed_nose/__init__.py index 4d9ed8f..45454ca 100644 --- a/distributed_nose/__init__.py +++ b/distributed_nose/__init__.py @@ -1,5 +1,5 @@ """Distribute your nose tests across multiple machines, hassle-free""" -VERSION = (0, 1, 2, '') +VERSION = (0, 2, 0, '') __version__ = '.'.join(map(str, VERSION[0:3])) + ''.join(VERSION[3:]) __author__ = 'Wes Winham' __contact__ = 'winhamwr@gmail.com' diff --git a/distributed_nose/plugin.py b/distributed_nose/plugin.py index ac65e93..fb80627 100644 --- a/distributed_nose/plugin.py +++ b/distributed_nose/plugin.py @@ -8,6 +8,7 @@ logger = logging.getLogger('nose.plugins.distributed_nose') + class DistributedNose(Plugin): """ Distribute a test run, shared-nothing style, by specifying the total number @@ -103,10 +104,34 @@ def _options_are_valid(self): return True - def validateName(self, testObject): + def getLowestSplitLevelObject(self, testObject): + """ + Get an object name to hash for determining which node will run this test. + If the containing module cannot be split, return the module name. + If the module can be split, but the containing class cannot, return the module dot class name. + If the module and class can be split, return the module.test name. + """ filepath, module, call = test_address(testObject) + if not getattr(module, '_distributed_can_split_', True): + return '%s' % module + + obj_self = getattr(testObject, '__self__', None) + klass = None + + if obj_self is not None: + klass = getattr(testObject, '__class__', None) + else: + if hasattr(testObject, 'im_class'): + klass = testObject.im_class - node = self.hash_ring.get_node('%s.%s' % (module, call)) + if klass is not None: + if not getattr(klass, '_distributed_can_split_', True): + return '%s.%s' % (module, klass) + + return '%s.%s' % (module, call) + + def validateName(self, testObject): + node = self.hash_ring.get_node(self.getLowestSplitLevelObject(testObject)) if node != self.node_id: return False diff --git a/setup.py b/setup.py index 0858049..2b5282d 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,6 @@ 'Programming Language :: Python', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.3', 'Operating System :: OS Independent', 'Operating System :: POSIX', 'Operating System :: Microsoft :: Windows', @@ -94,7 +93,7 @@ def add_doc(m): 'nose', 'hash_ring', ], - entry_points = { + entry_points={ 'nose.plugins.0.10': [ 'distributed = distributed_nose.plugin:DistributedNose', ], diff --git a/tests/dummy_tests.py b/tests/dummy_tests.py index 38b911f..140e5eb 100644 --- a/tests/dummy_tests.py +++ b/tests/dummy_tests.py @@ -1,6 +1,7 @@ import unittest + class TC1(unittest.TestCase): def test_method1(self): assert True @@ -19,8 +20,25 @@ class TC2(TC1): pass +class TC3(unittest.TestCase): + _distributed_can_split_ = False + + def test_method1(self): + assert True + + def test_method2(self): + assert True + + def test_method3(self): + assert True + + def test_method4(self): + assert True + + def test_func1(): assert True + def test_func2(): assert True diff --git a/tests/test_distribution.py b/tests/test_distribution.py index 0b2d57f..d85622b 100644 --- a/tests/test_distribution.py +++ b/tests/test_distribution.py @@ -6,7 +6,8 @@ from distributed_nose.plugin import DistributedNose -from tests.dummy_tests import TC1, TC2, test_func1, test_func2 +from tests.dummy_tests import TC1, TC2, TC3, test_func1, test_func2 + class TestTestSelection(unittest.TestCase): @@ -46,3 +47,51 @@ def test_not_all_tests_found(self): self.assertFalse(all_allowed) + def test_all_tests_found(self): + plug1 = self.plugin + plug2 = DistributedNose() + + plug1.options(self.parser, env={}) + args = ['--nodes=2', '--node-number=1'] + options, _ = self.parser.parse_args(args) + plug1.configure(options, Config()) + + self.parser = OptionParser() + plug2.options(self.parser, env={}) + args = ['--nodes=2', '--node-number=2'] + options, _ = self.parser.parse_args(args) + plug2.configure(options, Config()) + + all_allowed = True + + for test in [TC1, TC2, TC3, test_func1, test_func2]: + if not (plug1.validateName(test) is None or plug2.validateName(test) is None): + all_allowed = False + + self.assertTrue(all_allowed) + + def test_can_distribute(self): + plug1 = self.plugin + plug2 = DistributedNose() + + plug1.options(self.parser, env={}) + args = ['--nodes=2', '--node-number=1'] + options, _ = self.parser.parse_args(args) + plug1.configure(options, Config()) + + self.parser = OptionParser() + plug2.options(self.parser, env={}) + args = ['--nodes=2', '--node-number=2'] + options, _ = self.parser.parse_args(args) + plug2.configure(options, Config()) + + any_allowed_1 = False + any_allowed_2 = False + + for test in [TC3.test_method1, TC3.test_method2, TC3.test_method3, TC3.test_method4]: + if plug1.validateName(test) is None: + any_allowed_1 = True + if plug2.validateName(test) is None: + any_allowed_2 = True + + self.assertTrue(any_allowed_1 ^ any_allowed_2) diff --git a/tox.ini b/tox.ini index 460b1e4..51a00c0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27,py33,pypy,nose-0-11,nose-1-0,docs +envlist = py26,py27,pypy,nose-1-0,nose-1-3 [testenv] deps = @@ -7,19 +7,12 @@ deps = commands = python setup.py nosetests -[testenv:nose-0-11] -basepython = python2.6 -deps = - nose<1.0 - [testenv:nose-1-0] basepython = python2.6 deps = nose>=1.0,<1.1 -[testenv:docs] -changedir = docs +[testenv:nose-1-3] +basepython = python2.6 deps = - sphinx -commands = - sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html + nose>=1.3,<1.4