Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
Wes Winham <winhamwr@gmail.com>
Jeff Meadows <jrmeadows2@gmail.com>
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion distributed_nose/__init__.py
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
29 changes: 27 additions & 2 deletions distributed_nose/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -94,7 +93,7 @@ def add_doc(m):
'nose',
'hash_ring',
],
entry_points = {
entry_points={
'nose.plugins.0.10': [
'distributed = distributed_nose.plugin:DistributedNose',
],
Expand Down
18 changes: 18 additions & 0 deletions tests/dummy_tests.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

import unittest


class TC1(unittest.TestCase):
def test_method1(self):
assert True
Expand All @@ -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
51 changes: 50 additions & 1 deletion tests/test_distribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down Expand Up @@ -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)
15 changes: 4 additions & 11 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
[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 =
nose>=1.2.1,<1.3
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