Skip to content

Commit 9b0b5b1

Browse files
authored
Add --maxschedchunk CLI option (#857)
Maximum number of tests scheduled in one step. Setting it to 1 will force pytest to send tests to workers one by one - might be useful for a small number of slow tests. Larger numbers will allow the scheduler to submit consecutive chunks of tests to workers - allows reusing fixtures. Unlimited if not set. Fixes #855 Fixes #255
1 parent 7faa69a commit 9b0b5b1

File tree

4 files changed

+74
-2
lines changed

4 files changed

+74
-2
lines changed

Diff for: changelog/855.feature

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Users can now configure ``load`` scheduling precision using ``--maxschedchunk`` command
2+
line option.

Diff for: src/xdist/plugin.py

+13
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,19 @@ def pytest_addoption(parser):
153153
"on every test run."
154154
),
155155
)
156+
group.addoption(
157+
"--maxschedchunk",
158+
action="store",
159+
type=int,
160+
help=(
161+
"Maximum number of tests scheduled in one step for --dist=load. "
162+
"Setting it to 1 will force pytest to send tests to workers one by "
163+
"one - might be useful for a small number of slow tests. "
164+
"Larger numbers will allow the scheduler to submit consecutive "
165+
"chunks of tests to workers - allows reusing fixtures. "
166+
"Unlimited if not set."
167+
),
168+
)
156169

157170
parser.addini(
158171
"rsyncdirs",

Diff for: src/xdist/scheduler/load.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def __init__(self, config, log=None):
6464
else:
6565
self.log = log.loadsched
6666
self.config = config
67+
self.maxschedchunk = self.config.getoption("maxschedchunk")
6768

6869
@property
6970
def nodes(self):
@@ -185,7 +186,9 @@ def check_schedule(self, node, duration=0):
185186
# so let's rather wait with sending new items
186187
return
187188
num_send = items_per_node_max - len(node_pending)
188-
self._send_tests(node, num_send)
189+
# keep at least 2 tests pending even if --maxschedchunk=1
190+
maxschedchunk = max(2 - len(node_pending), self.maxschedchunk)
191+
self._send_tests(node, min(num_send, maxschedchunk))
189192
else:
190193
node.shutdown()
191194

@@ -245,6 +248,9 @@ def schedule(self):
245248
if not self.collection:
246249
return
247250

251+
if self.maxschedchunk is None:
252+
self.maxschedchunk = len(self.collection)
253+
248254
# Send a batch of tests to run. If we don't have at least two
249255
# tests per node, we have to send them all so that we can send
250256
# shutdown signals and get all nodes working.
@@ -265,7 +271,8 @@ def schedule(self):
265271
# how many items per node do we have about?
266272
items_per_node = len(self.collection) // len(self.node2pending)
267273
# take a fraction of tests for initial distribution
268-
node_chunksize = max(items_per_node // 4, 2)
274+
node_chunksize = min(items_per_node // 4, self.maxschedchunk)
275+
node_chunksize = max(node_chunksize, 2)
269276
# and initialize each node with a chunk of tests
270277
for node in self.nodes:
271278
self._send_tests(node, node_chunksize)

Diff for: testing/test_dsession.py

+50
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,56 @@ def test_schedule_batch_size(self, pytester: pytest.Pytester) -> None:
129129
assert node1.sent == [0, 1, 4, 5]
130130
assert not sched.pending
131131

132+
def test_schedule_maxchunk_none(self, pytester: pytest.Pytester) -> None:
133+
config = pytester.parseconfig("--tx=2*popen")
134+
sched = LoadScheduling(config)
135+
sched.add_node(MockNode())
136+
sched.add_node(MockNode())
137+
node1, node2 = sched.nodes
138+
col = [f"test{i}" for i in range(16)]
139+
sched.add_node_collection(node1, col)
140+
sched.add_node_collection(node2, col)
141+
sched.schedule()
142+
assert node1.sent == [0, 1]
143+
assert node2.sent == [2, 3]
144+
assert sched.pending == list(range(4, 16))
145+
assert sched.node2pending[node1] == node1.sent
146+
assert sched.node2pending[node2] == node2.sent
147+
sched.mark_test_complete(node1, 0)
148+
assert node1.sent == [0, 1, 4, 5]
149+
assert sched.pending == list(range(6, 16))
150+
sched.mark_test_complete(node1, 1)
151+
assert node1.sent == [0, 1, 4, 5]
152+
assert sched.pending == list(range(6, 16))
153+
154+
for i in range(7, 16):
155+
sched.mark_test_complete(node1, i - 3)
156+
assert node1.sent == [0, 1] + list(range(4, i))
157+
assert node2.sent == [2, 3]
158+
assert sched.pending == list(range(i, 16))
159+
160+
def test_schedule_maxchunk_1(self, pytester: pytest.Pytester) -> None:
161+
config = pytester.parseconfig("--tx=2*popen", "--maxschedchunk=1")
162+
sched = LoadScheduling(config)
163+
sched.add_node(MockNode())
164+
sched.add_node(MockNode())
165+
node1, node2 = sched.nodes
166+
col = [f"test{i}" for i in range(16)]
167+
sched.add_node_collection(node1, col)
168+
sched.add_node_collection(node2, col)
169+
sched.schedule()
170+
assert node1.sent == [0, 1]
171+
assert node2.sent == [2, 3]
172+
assert sched.pending == list(range(4, 16))
173+
assert sched.node2pending[node1] == node1.sent
174+
assert sched.node2pending[node2] == node2.sent
175+
176+
for complete_index, first_pending in enumerate(range(5, 16)):
177+
sched.mark_test_complete(node1, node1.sent[complete_index])
178+
assert node1.sent == [0, 1] + list(range(4, first_pending))
179+
assert node2.sent == [2, 3]
180+
assert sched.pending == list(range(first_pending, 16))
181+
132182
def test_schedule_fewer_tests_than_nodes(self, pytester: pytest.Pytester) -> None:
133183
config = pytester.parseconfig("--tx=2*popen")
134184
sched = LoadScheduling(config)

0 commit comments

Comments
 (0)