Issue node shutdown on empty workload

Remote pytest_runtestloop requires at least 2 tests, or a test
and a shutdown command. In case of inter test locking, for
example using a file lock, the tests could deadlock, since the
shutdown command would not be enqueued, and the last test of a
worker would never finish, and would not allow another worker
to make progress.
This commit is contained in:
Jan Kasiak
2018-09-30 13:59:35 -04:00
parent 86e2fb5a5a
commit fe06f27b8b
6 changed files with 57 additions and 0 deletions

1
changelog/351.bugfix Normal file
View File

@@ -0,0 +1 @@
Fix scheduling deadlock in case of inter-test locking.

View File

@@ -1132,6 +1132,56 @@ class TestFileScope:
assert c1 == c2 assert c1 == c2
class TestLocking:
_test_content = """
class TestClassName%s(object):
@classmethod
def setup_class(cls):
FILE_LOCK.acquire()
@classmethod
def teardown_class(cls):
FILE_LOCK.release()
def test_a(self):
pass
def test_b(self):
pass
def test_c(self):
pass
"""
test_file1 = """
import filelock
FILE_LOCK = filelock.FileLock("test.lock")
""" + (
(_test_content * 4) % ("A", "B", "C", "D")
)
@pytest.mark.parametrize("scope", ["each", "load", "loadscope", "loadfile", "no"])
def test_single_file(self, testdir, scope):
testdir.makepyfile(test_a=self.test_file1)
result = testdir.runpytest("-n2", "--dist=%s" % scope, "-v")
result.assert_outcomes(passed=(12 if scope != "each" else 12 * 2))
@pytest.mark.parametrize("scope", ["each", "load", "loadscope", "loadfile", "no"])
def test_multi_file(self, testdir, scope):
testdir.makepyfile(
test_a=self.test_file1,
test_b=self.test_file1,
test_c=self.test_file1,
test_d=self.test_file1,
)
result = testdir.runpytest("-n2", "--dist=%s" % scope, "-v")
result.assert_outcomes(passed=(48 if scope != "each" else 48 * 2))
def parse_tests_and_workers_from_output(lines): def parse_tests_and_workers_from_output(lines):
result = [] result = []
for line in lines: for line in lines:

View File

@@ -23,6 +23,7 @@ deps =
pytestmaster: git+https://github.com/pytest-dev/pytest.git@master pytestmaster: git+https://github.com/pytest-dev/pytest.git@master
pytestfeatures: git+https://github.com/pytest-dev/pytest.git@features pytestfeatures: git+https://github.com/pytest-dev/pytest.git@features
pexpect: pexpect pexpect: pexpect
filelock
platform= platform=
pexpect: linux|darwin pexpect: linux|darwin
commands= commands=

View File

@@ -126,6 +126,7 @@ class EachScheduling(object):
if not pending: if not pending:
pending[:] = range(len(self.node2collection[node])) pending[:] = range(len(self.node2collection[node]))
node.send_runtest_all() node.send_runtest_all()
node.shutdown()
else: else:
node.send_runtest_some(pending) node.send_runtest_some(pending)
self._started.append(node) self._started.append(node)

View File

@@ -178,6 +178,9 @@ class LoadScheduling(object):
return return
num_send = items_per_node_max - len(node_pending) num_send = items_per_node_max - len(node_pending)
self._send_tests(node, num_send) self._send_tests(node, num_send)
else:
node.shutdown()
self.log("num items waiting for node:", len(self.pending)) self.log("num items waiting for node:", len(self.pending))
def remove_node(self, node): def remove_node(self, node):

View File

@@ -306,6 +306,7 @@ class LoadScopeScheduling(object):
# Check that more work is available # Check that more work is available
if not self.workqueue: if not self.workqueue:
node.shutdown()
return return
self.log("Number of units waiting for node:", len(self.workqueue)) self.log("Number of units waiting for node:", len(self.workqueue))