@@ -1,6 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
import pytest
|
||||
|
||||
from xdist.remote import Producer
|
||||
from xdist.report import report_collection_diff
|
||||
from xdist.workermanage import parse_spec_config
|
||||
from xdist.workermanage import WorkerController
|
||||
|
||||
|
||||
class EachScheduling:
|
||||
@@ -17,13 +24,13 @@ class EachScheduling:
|
||||
assigned the remaining items from the removed node.
|
||||
"""
|
||||
|
||||
def __init__(self, config, log=None):
|
||||
def __init__(self, config: pytest.Config, log: Producer | None = None) -> None:
|
||||
self.config = config
|
||||
self.numnodes = len(parse_spec_config(config))
|
||||
self.node2collection = {}
|
||||
self.node2pending = {}
|
||||
self._started = []
|
||||
self._removed2pending = {}
|
||||
self.node2collection: dict[WorkerController, list[str]] = {}
|
||||
self.node2pending: dict[WorkerController, list[int]] = {}
|
||||
self._started: list[WorkerController] = []
|
||||
self._removed2pending: dict[WorkerController, list[int]] = {}
|
||||
if log is None:
|
||||
self.log = Producer("eachsched")
|
||||
else:
|
||||
@@ -31,12 +38,12 @@ class EachScheduling:
|
||||
self.collection_is_completed = False
|
||||
|
||||
@property
|
||||
def nodes(self):
|
||||
def nodes(self) -> list[WorkerController]:
|
||||
"""A list of all nodes in the scheduler."""
|
||||
return list(self.node2pending.keys())
|
||||
|
||||
@property
|
||||
def tests_finished(self):
|
||||
def tests_finished(self) -> bool:
|
||||
if not self.collection_is_completed:
|
||||
return False
|
||||
if self._removed2pending:
|
||||
@@ -47,7 +54,7 @@ class EachScheduling:
|
||||
return True
|
||||
|
||||
@property
|
||||
def has_pending(self):
|
||||
def has_pending(self) -> bool:
|
||||
"""Return True if there are pending test items.
|
||||
|
||||
This indicates that collection has finished and nodes are
|
||||
@@ -59,11 +66,13 @@ class EachScheduling:
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_node(self, node):
|
||||
def add_node(self, node: WorkerController) -> None:
|
||||
assert node not in self.node2pending
|
||||
self.node2pending[node] = []
|
||||
|
||||
def add_node_collection(self, node, collection):
|
||||
def add_node_collection(
|
||||
self, node: WorkerController, collection: Sequence[str]
|
||||
) -> None:
|
||||
"""Add the collected test items from a node.
|
||||
|
||||
Collection is complete once all nodes have submitted their
|
||||
@@ -97,26 +106,32 @@ class EachScheduling:
|
||||
self.node2pending[node] = pending
|
||||
break
|
||||
|
||||
def mark_test_complete(self, node, item_index, duration=0):
|
||||
def mark_test_complete(
|
||||
self, node: WorkerController, item_index: int, duration: float = 0
|
||||
) -> None:
|
||||
self.node2pending[node].remove(item_index)
|
||||
|
||||
def mark_test_pending(self, item):
|
||||
def mark_test_pending(self, item: str) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def remove_pending_tests_from_node(self, node, indices):
|
||||
def remove_pending_tests_from_node(
|
||||
self,
|
||||
node: WorkerController,
|
||||
indices: Sequence[int],
|
||||
) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def remove_node(self, node):
|
||||
def remove_node(self, node: WorkerController) -> str | None:
|
||||
# KeyError if we didn't get an add_node() yet
|
||||
pending = self.node2pending.pop(node)
|
||||
if not pending:
|
||||
return
|
||||
return None
|
||||
crashitem = self.node2collection[node][pending.pop(0)]
|
||||
if pending:
|
||||
self._removed2pending[node] = pending
|
||||
return crashitem
|
||||
|
||||
def schedule(self):
|
||||
def schedule(self) -> None:
|
||||
"""Schedule the test items on the nodes.
|
||||
|
||||
If the node's pending list is empty it is a new node which
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from itertools import cycle
|
||||
from typing import Sequence
|
||||
|
||||
import pytest
|
||||
|
||||
from xdist.remote import Producer
|
||||
from xdist.report import report_collection_diff
|
||||
from xdist.workermanage import parse_spec_config
|
||||
from xdist.workermanage import WorkerController
|
||||
|
||||
|
||||
class LoadScheduling:
|
||||
@@ -53,12 +57,12 @@ class LoadScheduling:
|
||||
:config: Config object, used for handling hooks.
|
||||
"""
|
||||
|
||||
def __init__(self, config, log=None):
|
||||
def __init__(self, config: pytest.Config, log: Producer | None = None) -> None:
|
||||
self.numnodes = len(parse_spec_config(config))
|
||||
self.node2collection = {}
|
||||
self.node2pending = {}
|
||||
self.pending = []
|
||||
self.collection = None
|
||||
self.node2collection: dict[WorkerController, list[str]] = {}
|
||||
self.node2pending: dict[WorkerController, list[int]] = {}
|
||||
self.pending: list[int] = []
|
||||
self.collection: list[str] | None = None
|
||||
if log is None:
|
||||
self.log = Producer("loadsched")
|
||||
else:
|
||||
@@ -67,12 +71,12 @@ class LoadScheduling:
|
||||
self.maxschedchunk = self.config.getoption("maxschedchunk")
|
||||
|
||||
@property
|
||||
def nodes(self):
|
||||
def nodes(self) -> list[WorkerController]:
|
||||
"""A list of all nodes in the scheduler."""
|
||||
return list(self.node2pending.keys())
|
||||
|
||||
@property
|
||||
def collection_is_completed(self):
|
||||
def collection_is_completed(self) -> bool:
|
||||
"""Boolean indication initial test collection is complete.
|
||||
|
||||
This is a boolean indicating all initial participating nodes
|
||||
@@ -82,7 +86,7 @@ class LoadScheduling:
|
||||
return len(self.node2collection) >= self.numnodes
|
||||
|
||||
@property
|
||||
def tests_finished(self):
|
||||
def tests_finished(self) -> bool:
|
||||
"""Return True if all tests have been executed by the nodes."""
|
||||
if not self.collection_is_completed:
|
||||
return False
|
||||
@@ -94,7 +98,7 @@ class LoadScheduling:
|
||||
return True
|
||||
|
||||
@property
|
||||
def has_pending(self):
|
||||
def has_pending(self) -> bool:
|
||||
"""Return True if there are pending test items.
|
||||
|
||||
This indicates that collection has finished and nodes are
|
||||
@@ -108,7 +112,7 @@ class LoadScheduling:
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_node(self, node):
|
||||
def add_node(self, node: WorkerController) -> None:
|
||||
"""Add a new node to the scheduler.
|
||||
|
||||
From now on the node will be allocated chunks of tests to
|
||||
@@ -120,7 +124,9 @@ class LoadScheduling:
|
||||
assert node not in self.node2pending
|
||||
self.node2pending[node] = []
|
||||
|
||||
def add_node_collection(self, node, collection):
|
||||
def add_node_collection(
|
||||
self, node: WorkerController, collection: Sequence[str]
|
||||
) -> None:
|
||||
"""Add the collected test items from a node.
|
||||
|
||||
The collection is stored in the ``.node2collection`` map.
|
||||
@@ -141,7 +147,9 @@ class LoadScheduling:
|
||||
return
|
||||
self.node2collection[node] = list(collection)
|
||||
|
||||
def mark_test_complete(self, node, item_index, duration=0):
|
||||
def mark_test_complete(
|
||||
self, node: WorkerController, item_index: int, duration: float = 0
|
||||
) -> None:
|
||||
"""Mark test item as completed by node.
|
||||
|
||||
The duration it took to execute the item is used as a hint to
|
||||
@@ -152,7 +160,8 @@ class LoadScheduling:
|
||||
self.node2pending[node].remove(item_index)
|
||||
self.check_schedule(node, duration=duration)
|
||||
|
||||
def mark_test_pending(self, item):
|
||||
def mark_test_pending(self, item: str) -> None:
|
||||
assert self.collection is not None
|
||||
self.pending.insert(
|
||||
0,
|
||||
self.collection.index(item),
|
||||
@@ -160,10 +169,14 @@ class LoadScheduling:
|
||||
for node in self.node2pending:
|
||||
self.check_schedule(node)
|
||||
|
||||
def remove_pending_tests_from_node(self, node, indices):
|
||||
def remove_pending_tests_from_node(
|
||||
self,
|
||||
node: WorkerController,
|
||||
indices: Sequence[int],
|
||||
) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def check_schedule(self, node, duration=0):
|
||||
def check_schedule(self, node: WorkerController, duration: float = 0) -> None:
|
||||
"""Maybe schedule new items on the node.
|
||||
|
||||
If there are any globally pending nodes left then this will
|
||||
@@ -197,7 +210,7 @@ class LoadScheduling:
|
||||
|
||||
self.log("num items waiting for node:", len(self.pending))
|
||||
|
||||
def remove_node(self, node):
|
||||
def remove_node(self, node: WorkerController) -> str | None:
|
||||
"""Remove a node from the scheduler.
|
||||
|
||||
This should be called either when the node crashed or at
|
||||
@@ -212,16 +225,17 @@ class LoadScheduling:
|
||||
"""
|
||||
pending = self.node2pending.pop(node)
|
||||
if not pending:
|
||||
return
|
||||
return None
|
||||
|
||||
# The node crashed, reassing pending items
|
||||
assert self.collection is not None
|
||||
crashitem = self.collection[pending.pop(0)]
|
||||
self.pending.extend(pending)
|
||||
for node in self.node2pending:
|
||||
self.check_schedule(node)
|
||||
return crashitem
|
||||
|
||||
def schedule(self):
|
||||
def schedule(self) -> None:
|
||||
"""Initiate distribution of the test collection.
|
||||
|
||||
Initiate scheduling of the items across the nodes. If this
|
||||
@@ -285,14 +299,14 @@ class LoadScheduling:
|
||||
for node in self.nodes:
|
||||
node.shutdown()
|
||||
|
||||
def _send_tests(self, node, num):
|
||||
def _send_tests(self, node: WorkerController, num: int) -> None:
|
||||
tests_per_node = self.pending[:num]
|
||||
if tests_per_node:
|
||||
del self.pending[:num]
|
||||
self.node2pending[node].extend(tests_per_node)
|
||||
node.send_runtest_some(tests_per_node)
|
||||
|
||||
def _check_nodes_have_same_collection(self):
|
||||
def _check_nodes_have_same_collection(self) -> bool:
|
||||
"""Return True if all nodes have collected the same items.
|
||||
|
||||
If collections differ, this method returns False while logging
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from xdist.remote import Producer
|
||||
|
||||
from .loadscope import LoadScopeScheduling
|
||||
@@ -21,14 +25,14 @@ class LoadFileScheduling(LoadScopeScheduling):
|
||||
This class behaves very much like LoadScopeScheduling, but with a file-level scope.
|
||||
"""
|
||||
|
||||
def __init__(self, config, log=None):
|
||||
def __init__(self, config: pytest.Config, log: Producer | None = None) -> None:
|
||||
super().__init__(config, log)
|
||||
if log is None:
|
||||
self.log = Producer("loadfilesched")
|
||||
else:
|
||||
self.log = log.loadfilesched
|
||||
|
||||
def _split_scope(self, nodeid):
|
||||
def _split_scope(self, nodeid: str) -> str:
|
||||
"""Determine the scope (grouping) of a nodeid.
|
||||
|
||||
There are usually 3 cases for a nodeid::
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from xdist.remote import Producer
|
||||
|
||||
from .loadscope import LoadScopeScheduling
|
||||
@@ -10,14 +14,14 @@ class LoadGroupScheduling(LoadScopeScheduling):
|
||||
instead of the module or class to which they belong to.
|
||||
"""
|
||||
|
||||
def __init__(self, config, log=None):
|
||||
def __init__(self, config: pytest.Config, log: Producer | None = None) -> None:
|
||||
super().__init__(config, log)
|
||||
if log is None:
|
||||
self.log = Producer("loadgroupsched")
|
||||
else:
|
||||
self.log = log.loadgroupsched
|
||||
|
||||
def _split_scope(self, nodeid):
|
||||
def _split_scope(self, nodeid: str) -> str:
|
||||
"""Determine the scope (grouping) of a nodeid.
|
||||
|
||||
There are usually 3 cases for a nodeid::
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
from typing import NoReturn
|
||||
from typing import Sequence
|
||||
|
||||
import pytest
|
||||
|
||||
from xdist.remote import Producer
|
||||
from xdist.report import report_collection_diff
|
||||
from xdist.workermanage import parse_spec_config
|
||||
from xdist.workermanage import WorkerController
|
||||
|
||||
|
||||
class LoadScopeScheduling:
|
||||
@@ -85,13 +90,13 @@ class LoadScopeScheduling:
|
||||
:config: Config object, used for handling hooks.
|
||||
"""
|
||||
|
||||
def __init__(self, config, log=None):
|
||||
def __init__(self, config: pytest.Config, log: Producer | None = None) -> None:
|
||||
self.numnodes = len(parse_spec_config(config))
|
||||
self.collection = None
|
||||
self.collection: list[str] | None = None
|
||||
|
||||
self.workqueue = OrderedDict()
|
||||
self.assigned_work = {}
|
||||
self.registered_collections = {}
|
||||
self.workqueue: OrderedDict[str, dict[str, bool]] = OrderedDict()
|
||||
self.assigned_work: dict[WorkerController, dict[str, dict[str, bool]]] = {}
|
||||
self.registered_collections: dict[WorkerController, list[str]] = {}
|
||||
|
||||
if log is None:
|
||||
self.log = Producer("loadscopesched")
|
||||
@@ -101,12 +106,12 @@ class LoadScopeScheduling:
|
||||
self.config = config
|
||||
|
||||
@property
|
||||
def nodes(self):
|
||||
def nodes(self) -> list[WorkerController]:
|
||||
"""A list of all active nodes in the scheduler."""
|
||||
return list(self.assigned_work.keys())
|
||||
|
||||
@property
|
||||
def collection_is_completed(self):
|
||||
def collection_is_completed(self) -> bool:
|
||||
"""Boolean indication initial test collection is complete.
|
||||
|
||||
This is a boolean indicating all initial participating nodes have
|
||||
@@ -116,7 +121,7 @@ class LoadScopeScheduling:
|
||||
return len(self.registered_collections) >= self.numnodes
|
||||
|
||||
@property
|
||||
def tests_finished(self):
|
||||
def tests_finished(self) -> bool:
|
||||
"""Return True if all tests have been executed by the nodes."""
|
||||
if not self.collection_is_completed:
|
||||
return False
|
||||
@@ -131,7 +136,7 @@ class LoadScopeScheduling:
|
||||
return True
|
||||
|
||||
@property
|
||||
def has_pending(self):
|
||||
def has_pending(self) -> bool:
|
||||
"""Return True if there are pending test items.
|
||||
|
||||
This indicates that collection has finished and nodes are still
|
||||
@@ -147,7 +152,7 @@ class LoadScopeScheduling:
|
||||
|
||||
return False
|
||||
|
||||
def add_node(self, node):
|
||||
def add_node(self, node: WorkerController) -> None:
|
||||
"""Add a new node to the scheduler.
|
||||
|
||||
From now on the node will be assigned work units to be executed.
|
||||
@@ -158,7 +163,7 @@ class LoadScopeScheduling:
|
||||
assert node not in self.assigned_work
|
||||
self.assigned_work[node] = {}
|
||||
|
||||
def remove_node(self, node):
|
||||
def remove_node(self, node: WorkerController) -> str | None:
|
||||
"""Remove a node from the scheduler.
|
||||
|
||||
This should be called either when the node crashed or at shutdown time.
|
||||
@@ -199,7 +204,9 @@ class LoadScopeScheduling:
|
||||
|
||||
return crashitem
|
||||
|
||||
def add_node_collection(self, node, collection):
|
||||
def add_node_collection(
|
||||
self, node: WorkerController, collection: Sequence[str]
|
||||
) -> None:
|
||||
"""Add the collected test items from a node.
|
||||
|
||||
The collection is stored in the ``.registered_collections`` dictionary.
|
||||
@@ -228,7 +235,9 @@ class LoadScopeScheduling:
|
||||
|
||||
self.registered_collections[node] = list(collection)
|
||||
|
||||
def mark_test_complete(self, node, item_index, duration=0):
|
||||
def mark_test_complete(
|
||||
self, node: WorkerController, item_index: int, duration: float = 0
|
||||
) -> None:
|
||||
"""Mark test item as completed by node.
|
||||
|
||||
Called by the hook:
|
||||
@@ -241,13 +250,17 @@ class LoadScopeScheduling:
|
||||
self.assigned_work[node][scope][nodeid] = True
|
||||
self._reschedule(node)
|
||||
|
||||
def mark_test_pending(self, item):
|
||||
def mark_test_pending(self, item: str) -> NoReturn:
|
||||
raise NotImplementedError()
|
||||
|
||||
def remove_pending_tests_from_node(self, node, indices):
|
||||
def remove_pending_tests_from_node(
|
||||
self,
|
||||
node: WorkerController,
|
||||
indices: Sequence[int],
|
||||
) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def _assign_work_unit(self, node):
|
||||
def _assign_work_unit(self, node: WorkerController) -> None:
|
||||
"""Assign a work unit to a node."""
|
||||
assert self.workqueue
|
||||
|
||||
@@ -268,7 +281,7 @@ class LoadScopeScheduling:
|
||||
|
||||
node.send_runtest_some(nodeids_indexes)
|
||||
|
||||
def _split_scope(self, nodeid):
|
||||
def _split_scope(self, nodeid: str) -> str:
|
||||
"""Determine the scope (grouping) of a nodeid.
|
||||
|
||||
There are usually 3 cases for a nodeid::
|
||||
@@ -292,12 +305,12 @@ class LoadScopeScheduling:
|
||||
"""
|
||||
return nodeid.rsplit("::", 1)[0]
|
||||
|
||||
def _pending_of(self, workload):
|
||||
def _pending_of(self, workload: dict[str, dict[str, bool]]) -> int:
|
||||
"""Return the number of pending tests in a workload."""
|
||||
pending = sum(list(scope.values()).count(False) for scope in workload.values())
|
||||
return pending
|
||||
|
||||
def _reschedule(self, node):
|
||||
def _reschedule(self, node: WorkerController) -> None:
|
||||
"""Maybe schedule new items on the node.
|
||||
|
||||
If there are any globally pending work units left then this will check
|
||||
@@ -322,7 +335,7 @@ class LoadScopeScheduling:
|
||||
# Pop one unit of work and assign it
|
||||
self._assign_work_unit(node)
|
||||
|
||||
def schedule(self):
|
||||
def schedule(self) -> None:
|
||||
"""Initiate distribution of the test collection.
|
||||
|
||||
Initiate scheduling of the items across the nodes. If this gets called
|
||||
@@ -352,7 +365,7 @@ class LoadScopeScheduling:
|
||||
return
|
||||
|
||||
# Determine chunks of work (scopes)
|
||||
unsorted_workqueue = {}
|
||||
unsorted_workqueue: dict[str, dict[str, bool]] = {}
|
||||
for nodeid in self.collection:
|
||||
scope = self._split_scope(nodeid)
|
||||
work_unit = unsorted_workqueue.setdefault(scope, {})
|
||||
@@ -389,7 +402,7 @@ class LoadScopeScheduling:
|
||||
for node in self.nodes:
|
||||
node.shutdown()
|
||||
|
||||
def _check_nodes_have_same_collection(self):
|
||||
def _check_nodes_have_same_collection(self) -> bool:
|
||||
"""Return True if all nodes have collected the same items.
|
||||
|
||||
If collections differ, this method returns False while logging
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import NamedTuple
|
||||
from typing import Sequence
|
||||
|
||||
import pytest
|
||||
|
||||
from xdist.remote import Producer
|
||||
from xdist.report import report_collection_diff
|
||||
from xdist.workermanage import parse_spec_config
|
||||
from xdist.workermanage import WorkerController
|
||||
|
||||
|
||||
class NodePending(NamedTuple):
|
||||
node: Any
|
||||
node: WorkerController
|
||||
pending: list[int]
|
||||
|
||||
|
||||
@@ -63,26 +64,26 @@ class WorkStealingScheduling:
|
||||
simultaneous requests.
|
||||
"""
|
||||
|
||||
def __init__(self, config, log=None):
|
||||
def __init__(self, config: pytest.Config, log: Producer | None = None) -> None:
|
||||
self.numnodes = len(parse_spec_config(config))
|
||||
self.node2collection = {}
|
||||
self.node2pending = {}
|
||||
self.pending = []
|
||||
self.collection = None
|
||||
self.node2collection: dict[WorkerController, list[str]] = {}
|
||||
self.node2pending: dict[WorkerController, list[int]] = {}
|
||||
self.pending: list[int] = []
|
||||
self.collection: list[str] | None = None
|
||||
if log is None:
|
||||
self.log = Producer("workstealsched")
|
||||
else:
|
||||
self.log = log.workstealsched
|
||||
self.config = config
|
||||
self.steal_requested_from_node = None
|
||||
self.steal_requested_from_node: WorkerController | None = None
|
||||
|
||||
@property
|
||||
def nodes(self):
|
||||
def nodes(self) -> list[WorkerController]:
|
||||
"""A list of all nodes in the scheduler."""
|
||||
return list(self.node2pending.keys())
|
||||
|
||||
@property
|
||||
def collection_is_completed(self):
|
||||
def collection_is_completed(self) -> bool:
|
||||
"""Boolean indication initial test collection is complete.
|
||||
|
||||
This is a boolean indicating all initial participating nodes
|
||||
@@ -92,7 +93,7 @@ class WorkStealingScheduling:
|
||||
return len(self.node2collection) >= self.numnodes
|
||||
|
||||
@property
|
||||
def tests_finished(self):
|
||||
def tests_finished(self) -> bool:
|
||||
"""Return True if all tests have been executed by the nodes."""
|
||||
if not self.collection_is_completed:
|
||||
return False
|
||||
@@ -106,7 +107,7 @@ class WorkStealingScheduling:
|
||||
return True
|
||||
|
||||
@property
|
||||
def has_pending(self):
|
||||
def has_pending(self) -> bool:
|
||||
"""Return True if there are pending test items.
|
||||
|
||||
This indicates that collection has finished and nodes are
|
||||
@@ -120,7 +121,7 @@ class WorkStealingScheduling:
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_node(self, node):
|
||||
def add_node(self, node: WorkerController) -> None:
|
||||
"""Add a new node to the scheduler.
|
||||
|
||||
From now on the node will be allocated chunks of tests to
|
||||
@@ -132,7 +133,9 @@ class WorkStealingScheduling:
|
||||
assert node not in self.node2pending
|
||||
self.node2pending[node] = []
|
||||
|
||||
def add_node_collection(self, node, collection):
|
||||
def add_node_collection(
|
||||
self, node: WorkerController, collection: Sequence[str]
|
||||
) -> None:
|
||||
"""Add the collected test items from a node.
|
||||
|
||||
The collection is stored in the ``.node2collection`` map.
|
||||
@@ -153,7 +156,9 @@ class WorkStealingScheduling:
|
||||
return
|
||||
self.node2collection[node] = list(collection)
|
||||
|
||||
def mark_test_complete(self, node, item_index, duration=None):
|
||||
def mark_test_complete(
|
||||
self, node: WorkerController, item_index: int, duration: float | None = None
|
||||
) -> None:
|
||||
"""Mark test item as completed by node.
|
||||
|
||||
This is called by the ``DSession.worker_testreport`` hook.
|
||||
@@ -161,14 +166,19 @@ class WorkStealingScheduling:
|
||||
self.node2pending[node].remove(item_index)
|
||||
self.check_schedule()
|
||||
|
||||
def mark_test_pending(self, item):
|
||||
def mark_test_pending(self, item: str) -> None:
|
||||
assert self.collection is not None
|
||||
self.pending.insert(
|
||||
0,
|
||||
self.collection.index(item),
|
||||
)
|
||||
self.check_schedule()
|
||||
|
||||
def remove_pending_tests_from_node(self, node, indices):
|
||||
def remove_pending_tests_from_node(
|
||||
self,
|
||||
node: WorkerController,
|
||||
indices: Sequence[int],
|
||||
) -> None:
|
||||
"""Node returned some test indices back in response to 'steal' command.
|
||||
|
||||
This is called by ``DSession.worker_unscheduled``.
|
||||
@@ -183,7 +193,7 @@ class WorkStealingScheduling:
|
||||
self.pending.extend(indices)
|
||||
self.check_schedule()
|
||||
|
||||
def check_schedule(self):
|
||||
def check_schedule(self) -> None:
|
||||
"""Reschedule tests/perform load balancing."""
|
||||
nodes_up = [
|
||||
NodePending(node, pending)
|
||||
@@ -191,7 +201,7 @@ class WorkStealingScheduling:
|
||||
if not node.shutting_down
|
||||
]
|
||||
|
||||
def get_idle_nodes():
|
||||
def get_idle_nodes() -> list[WorkerController]:
|
||||
return [node for node, pending in nodes_up if len(pending) < MIN_PENDING]
|
||||
|
||||
idle_nodes = get_idle_nodes()
|
||||
@@ -235,10 +245,11 @@ class WorkStealingScheduling:
|
||||
node.shutdown()
|
||||
return
|
||||
|
||||
assert steal_from is not None
|
||||
steal_from.node.send_steal(steal_from.pending[-num_steal:])
|
||||
self.steal_requested_from_node = steal_from.node
|
||||
|
||||
def remove_node(self, node):
|
||||
def remove_node(self, node: WorkerController) -> str | None:
|
||||
"""Remove a node from the scheduler.
|
||||
|
||||
This should be called either when the node crashed or at
|
||||
@@ -249,12 +260,12 @@ class WorkStealingScheduling:
|
||||
|
||||
Return the item which was being executing while the node
|
||||
crashed or None if the node has no more pending items.
|
||||
|
||||
"""
|
||||
pending = self.node2pending.pop(node)
|
||||
|
||||
# If node was removed without completing its assigned tests - it crashed
|
||||
if pending:
|
||||
assert self.collection is not None
|
||||
crashitem = self.collection[pending.pop(0)]
|
||||
else:
|
||||
crashitem = None
|
||||
@@ -268,7 +279,7 @@ class WorkStealingScheduling:
|
||||
self.check_schedule()
|
||||
return crashitem
|
||||
|
||||
def schedule(self):
|
||||
def schedule(self) -> None:
|
||||
"""Initiate distribution of the test collection.
|
||||
|
||||
Initiate scheduling of the items across the nodes. If this
|
||||
@@ -298,14 +309,14 @@ class WorkStealingScheduling:
|
||||
|
||||
self.check_schedule()
|
||||
|
||||
def _send_tests(self, node, num):
|
||||
def _send_tests(self, node: WorkerController, num: int) -> None:
|
||||
tests_per_node = self.pending[:num]
|
||||
if tests_per_node:
|
||||
del self.pending[:num]
|
||||
self.node2pending[node].extend(tests_per_node)
|
||||
node.send_runtest_some(tests_per_node)
|
||||
|
||||
def _check_nodes_have_same_collection(self):
|
||||
def _check_nodes_have_same_collection(self) -> bool:
|
||||
"""Return True if all nodes have collected the same items.
|
||||
|
||||
If collections differ, this method returns False while logging
|
||||
|
||||
Reference in New Issue
Block a user