fix pytest issue503: avoid random re-setup of broad scoped fixtures
(anything above function).
This commit is contained in:
@@ -1,9 +1,14 @@
|
|||||||
XXX
|
1.11
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
- fix pytest/xdist issue485 (also depends on py-1.4.21.dev1):
|
- fix pytest/xdist issue485 (also depends on py-1.4.22):
|
||||||
attach stdout/stderr on --boxed processes that die.
|
attach stdout/stderr on --boxed processes that die.
|
||||||
|
|
||||||
|
- fix pytest/xdist issue503: make sure that a node has usually
|
||||||
|
two items to execute to avoid scoped fixtures to be torn down
|
||||||
|
pre-maturely (fixture teardown/setup is "nextitem" sensitive).
|
||||||
|
Thanks to Andreas Pelme for bug analysis and failing test.
|
||||||
|
|
||||||
1.10
|
1.10
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import pytest
|
||||||
import py
|
import py
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -459,3 +460,29 @@ def test_issue34_pluginloading_in_subprocess(testdir):
|
|||||||
result.stdout.fnmatch_lines([
|
result.stdout.fnmatch_lines([
|
||||||
"*1 passed*",
|
"*1 passed*",
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def test_fixture_scope_caching_issue503(testdir):
|
||||||
|
p1 = testdir.makepyfile("""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def fix():
|
||||||
|
assert fix.counter == 0, 'session fixture was invoked multiple times'
|
||||||
|
fix.counter += 1
|
||||||
|
fix.counter = 0
|
||||||
|
|
||||||
|
def test_a(fix):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_b(fix):
|
||||||
|
pass
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest(p1, '-v', '-n1')
|
||||||
|
assert result.ret == 0
|
||||||
|
result.stdout.fnmatch_lines([
|
||||||
|
"*2 passed*",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -98,15 +98,15 @@ class TestLoadScheduling:
|
|||||||
assert sched.node2collection[node1] == collection
|
assert sched.node2collection[node1] == collection
|
||||||
assert sched.node2collection[node2] == collection
|
assert sched.node2collection[node2] == collection
|
||||||
sched.init_distribute()
|
sched.init_distribute()
|
||||||
assert sched.tests_finished()
|
|
||||||
assert len(node1.sent) == 1
|
|
||||||
assert len(node2.sent) == 1
|
|
||||||
x = sorted(node1.sent + node2.sent)
|
|
||||||
assert x == [0, 1]
|
|
||||||
sched.remove_item(node1, node1.sent[0])
|
|
||||||
sched.remove_item(node2, node2.sent[0])
|
|
||||||
assert sched.tests_finished()
|
|
||||||
assert not sched.pending
|
assert not sched.pending
|
||||||
|
assert not sched.tests_finished()
|
||||||
|
assert len(node1.sent) == 2
|
||||||
|
assert len(node2.sent) == 0
|
||||||
|
assert node1.sent == [0, 1]
|
||||||
|
sched.remove_item(node1, node1.sent[0])
|
||||||
|
assert sched.tests_finished()
|
||||||
|
sched.remove_item(node1, node1.sent[1])
|
||||||
|
assert sched.tests_finished()
|
||||||
|
|
||||||
def test_init_distribute_chunksize(self):
|
def test_init_distribute_chunksize(self):
|
||||||
sched = LoadScheduling(2)
|
sched = LoadScheduling(2)
|
||||||
@@ -114,22 +114,25 @@ class TestLoadScheduling:
|
|||||||
node2 = MockNode()
|
node2 = MockNode()
|
||||||
sched.addnode(node1)
|
sched.addnode(node1)
|
||||||
sched.addnode(node2)
|
sched.addnode(node2)
|
||||||
col = ["xyz"] * (3)
|
col = ["xyz"] * (6)
|
||||||
sched.addnode_collection(node1, col)
|
sched.addnode_collection(node1, col)
|
||||||
sched.addnode_collection(node2, col)
|
sched.addnode_collection(node2, col)
|
||||||
sched.init_distribute()
|
sched.init_distribute()
|
||||||
#assert not sched.tests_finished()
|
#assert not sched.tests_finished()
|
||||||
sent1 = node1.sent
|
sent1 = node1.sent
|
||||||
sent2 = node2.sent
|
sent2 = node2.sent
|
||||||
chunkitems = col[:1]
|
assert sent1 == [0, 1]
|
||||||
assert (sent1 == [0] and sent2 == [1]) or (
|
assert sent2 == [2, 3]
|
||||||
sent1 == [1] and sent2 == [0])
|
assert sched.pending == [4, 5]
|
||||||
assert sched.node2pending[node1] == sent1
|
assert sched.node2pending[node1] == sent1
|
||||||
assert sched.node2pending[node2] == sent2
|
assert sched.node2pending[node2] == sent2
|
||||||
assert len(sched.pending) == 1
|
assert len(sched.pending) == 2
|
||||||
for node in (node1, node2):
|
sched.remove_item(node1, 0)
|
||||||
for i in sched.node2pending[node]:
|
assert node1.sent == [0, 1, 4]
|
||||||
sched.remove_item(node, i)
|
assert sched.pending == [5]
|
||||||
|
assert node2.sent == [2, 3]
|
||||||
|
sched.remove_item(node1, 1)
|
||||||
|
assert node1.sent == [0, 1, 4, 5]
|
||||||
assert not sched.pending
|
assert not sched.pending
|
||||||
|
|
||||||
def test_add_remove_node(self):
|
def test_add_remove_node(self):
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class EachScheduling:
|
|||||||
if not pending:
|
if not pending:
|
||||||
return
|
return
|
||||||
crashitem = self.node2collection[node][pending.pop(0)]
|
crashitem = self.node2collection[node][pending.pop(0)]
|
||||||
# XXX what about the rest of pending?
|
# XXX do or report something wrt the remaining per-node pending items?
|
||||||
return crashitem
|
return crashitem
|
||||||
|
|
||||||
def init_distribute(self):
|
def init_distribute(self):
|
||||||
@@ -58,11 +58,13 @@ class EachScheduling:
|
|||||||
node.send_runtest_all()
|
node.send_runtest_all()
|
||||||
pending[:] = range(len(self.node2collection[node]))
|
pending[:] = range(len(self.node2collection[node]))
|
||||||
|
|
||||||
|
|
||||||
class LoadScheduling:
|
class LoadScheduling:
|
||||||
def __init__(self, numnodes, log=None):
|
def __init__(self, numnodes, log=None):
|
||||||
self.numnodes = numnodes
|
self.numnodes = numnodes
|
||||||
self.node2pending = {}
|
self.node2pending = {}
|
||||||
self.node2collection = {}
|
self.node2collection = {}
|
||||||
|
self.nodes = []
|
||||||
self.pending = []
|
self.pending = []
|
||||||
if log is None:
|
if log is None:
|
||||||
self.log = py.log.Producer("loadsched")
|
self.log = py.log.Producer("loadsched")
|
||||||
@@ -75,13 +77,14 @@ class LoadScheduling:
|
|||||||
|
|
||||||
def addnode(self, node):
|
def addnode(self, node):
|
||||||
self.node2pending[node] = []
|
self.node2pending[node] = []
|
||||||
|
self.nodes.append(node)
|
||||||
|
|
||||||
def tests_finished(self):
|
def tests_finished(self):
|
||||||
if not self.collection_is_completed or self.pending:
|
if not self.collection_is_completed:
|
||||||
|
return False
|
||||||
|
for pending in self.node2pending.values():
|
||||||
|
if len(pending) >= 2:
|
||||||
return False
|
return False
|
||||||
#for items in self.node2pending.values():
|
|
||||||
# if items:
|
|
||||||
# return False
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def addnode_collection(self, node, collection):
|
def addnode_collection(self, node, collection):
|
||||||
@@ -92,37 +95,46 @@ class LoadScheduling:
|
|||||||
self.collection_is_completed = True
|
self.collection_is_completed = True
|
||||||
|
|
||||||
def remove_item(self, node, item_index, duration=0):
|
def remove_item(self, node, item_index, duration=0):
|
||||||
node_pending = self.node2pending[node]
|
self.node2pending[node].remove(item_index)
|
||||||
node_pending.remove(item_index)
|
self.check_schedule(node, duration=duration)
|
||||||
# pre-load items-to-test if the node may become ready
|
|
||||||
|
|
||||||
|
def check_schedule(self, node, duration=0):
|
||||||
if self.pending:
|
if self.pending:
|
||||||
if duration >= 0.1 and node_pending:
|
# how many nodes do we have?
|
||||||
# seems the node is doing long-running tests
|
|
||||||
# so let's rather wait with sending new items
|
|
||||||
return
|
|
||||||
# how many nodes do we have remaining per node roughly?
|
|
||||||
num_nodes = len(self.node2pending)
|
num_nodes = len(self.node2pending)
|
||||||
# if our node goes below a heuristic minimum, fill it out to
|
# if our node goes below a heuristic minimum, fill it out to
|
||||||
# heuristic maximum
|
# heuristic maximum
|
||||||
items_per_node_min = max(
|
items_per_node_min = max(
|
||||||
1, len(self.pending) // num_nodes // 4)
|
2, len(self.pending) // num_nodes // 4)
|
||||||
items_per_node_max = max(
|
items_per_node_max = max(
|
||||||
1, len(self.pending) // num_nodes // 2)
|
2, len(self.pending) // num_nodes // 2)
|
||||||
if len(node_pending) <= items_per_node_min:
|
node_pending = self.node2pending[node]
|
||||||
num_send = items_per_node_max - len(node_pending) + 1
|
if len(node_pending) < items_per_node_min:
|
||||||
|
if duration >= 0.1 and len(node_pending) >= 2:
|
||||||
|
# seems the node is doing long-running tests
|
||||||
|
# and has enough items to continue
|
||||||
|
# so let's rather wait with sending new items
|
||||||
|
return
|
||||||
|
num_send = items_per_node_max - len(node_pending)
|
||||||
self._send_tests(node, num_send)
|
self._send_tests(node, num_send)
|
||||||
|
|
||||||
self.log("num items waiting for node:", len(self.pending))
|
self.log("num items waiting for node:", len(self.pending))
|
||||||
#self.log("node2pending:", self.node2pending)
|
#self.log("node2pending:", self.node2pending)
|
||||||
|
|
||||||
def remove_node(self, node):
|
def remove_node(self, node):
|
||||||
|
self.nodes.remove(node)
|
||||||
pending = self.node2pending.pop(node)
|
pending = self.node2pending.pop(node)
|
||||||
if not pending:
|
if not pending:
|
||||||
return
|
return
|
||||||
# the node must have crashed on the item if there are pending ones
|
# the node has crashed on the item if there are pending ones
|
||||||
|
# and we are told to remove the node
|
||||||
crashitem = self.collection[pending.pop(0)]
|
crashitem = self.collection[pending.pop(0)]
|
||||||
|
|
||||||
|
# put the remaining items back to the general pending list
|
||||||
self.pending.extend(pending)
|
self.pending.extend(pending)
|
||||||
|
# see if some nodes can pick the remaining tests up already
|
||||||
|
for node in self.node2pending:
|
||||||
|
self.check_schedule(node)
|
||||||
return crashitem
|
return crashitem
|
||||||
|
|
||||||
def init_distribute(self):
|
def init_distribute(self):
|
||||||
@@ -147,9 +159,9 @@ class LoadScheduling:
|
|||||||
# how many items per node do we have about?
|
# how many items per node do we have about?
|
||||||
items_per_node = len(self.collection) // len(self.node2pending)
|
items_per_node = len(self.collection) // len(self.node2pending)
|
||||||
# take a fraction of tests for initial distribution
|
# take a fraction of tests for initial distribution
|
||||||
node_chunksize = max(items_per_node // 4, 1)
|
node_chunksize = max(items_per_node // 4, 2)
|
||||||
# and initialize each node with a chunk of tests
|
# and initialize each node with a chunk of tests
|
||||||
for node in self.node2pending:
|
for node in self.nodes:
|
||||||
self._send_tests(node, node_chunksize)
|
self._send_tests(node, node_chunksize)
|
||||||
|
|
||||||
#f = open("/tmp/sent", "w")
|
#f = open("/tmp/sent", "w")
|
||||||
|
|||||||
@@ -51,9 +51,12 @@ class SlaveInteractor:
|
|||||||
elif name == "runtests_all":
|
elif name == "runtests_all":
|
||||||
torun.extend(range(len(session.items)))
|
torun.extend(range(len(session.items)))
|
||||||
self.log("items to run:", torun)
|
self.log("items to run:", torun)
|
||||||
while torun:
|
# only run if we have an item and a next item
|
||||||
|
while len(torun) >= 2:
|
||||||
self.run_tests(torun)
|
self.run_tests(torun)
|
||||||
if name == "shutdown":
|
if name == "shutdown":
|
||||||
|
if torun:
|
||||||
|
self.run_tests(torun)
|
||||||
break
|
break
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user