@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -223,7 +224,7 @@ class TestDistribution:
|
||||
assert result.ret == 1
|
||||
|
||||
def test_distribution_rsyncdirs_example(
|
||||
self, pytester: pytest.Pytester, monkeypatch
|
||||
self, pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
# use a custom plugin that has a custom command-line option to ensure
|
||||
# this is propagated to workers (see #491)
|
||||
@@ -415,7 +416,7 @@ class TestDistEach:
|
||||
|
||||
class TestTerminalReporting:
|
||||
@pytest.mark.parametrize("verbosity", ["", "-q", "-v"])
|
||||
def test_output_verbosity(self, pytester, verbosity: str) -> None:
|
||||
def test_output_verbosity(self, pytester: pytest.Pytester, verbosity: str) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def test_ok():
|
||||
@@ -610,7 +611,7 @@ def test_fixture_teardown_failure(pytester: pytest.Pytester) -> None:
|
||||
|
||||
|
||||
def test_config_initialization(
|
||||
pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch, pytestconfig
|
||||
pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Ensure workers and controller are initialized consistently. Integration test for #445."""
|
||||
pytester.makepyfile(
|
||||
@@ -635,7 +636,7 @@ def test_config_initialization(
|
||||
|
||||
|
||||
@pytest.mark.parametrize("when", ["setup", "call", "teardown"])
|
||||
def test_crashing_item(pytester, when) -> None:
|
||||
def test_crashing_item(pytester: pytest.Pytester, when: str) -> None:
|
||||
"""Ensure crashing item is correctly reported during all testing stages."""
|
||||
code = dict(setup="", call="", teardown="")
|
||||
code[when] = "os._exit(1)"
|
||||
@@ -766,7 +767,7 @@ def test_tmpdir_disabled(pytester: pytest.Pytester) -> None:
|
||||
|
||||
|
||||
@pytest.mark.parametrize("plugin", ["xdist.looponfail"])
|
||||
def test_sub_plugins_disabled(pytester, plugin) -> None:
|
||||
def test_sub_plugins_disabled(pytester: pytest.Pytester, plugin: str) -> None:
|
||||
"""Test that xdist doesn't break if we disable any of its sub-plugins (#32)."""
|
||||
p1 = pytester.makepyfile(
|
||||
"""
|
||||
@@ -781,7 +782,7 @@ def test_sub_plugins_disabled(pytester, plugin) -> None:
|
||||
|
||||
class TestWarnings:
|
||||
@pytest.mark.parametrize("n", ["-n0", "-n1"])
|
||||
def test_warnings(self, pytester, n) -> None:
|
||||
def test_warnings(self, pytester: pytest.Pytester, n: str) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import warnings, py, pytest
|
||||
@@ -827,7 +828,7 @@ class TestWarnings:
|
||||
result.stdout.no_fnmatch_line("*this hook should not be called in this version")
|
||||
|
||||
@pytest.mark.parametrize("n", ["-n0", "-n1"])
|
||||
def test_custom_subclass(self, pytester, n) -> None:
|
||||
def test_custom_subclass(self, pytester: pytest.Pytester, n: str) -> None:
|
||||
"""Check that warning subclasses that don't honor the args attribute don't break
|
||||
pytest-xdist (#344).
|
||||
"""
|
||||
@@ -851,7 +852,7 @@ class TestWarnings:
|
||||
result.stdout.fnmatch_lines(["*MyWarning*", "*1 passed, 1 warning*"])
|
||||
|
||||
@pytest.mark.parametrize("n", ["-n0", "-n1"])
|
||||
def test_unserializable_arguments(self, pytester, n) -> None:
|
||||
def test_unserializable_arguments(self, pytester: pytest.Pytester, n: str) -> None:
|
||||
"""Check that warnings with unserializable arguments are handled correctly (#349)."""
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
@@ -869,7 +870,9 @@ class TestWarnings:
|
||||
result.stdout.fnmatch_lines(["*UserWarning*foo.txt*", "*1 passed, 1 warning*"])
|
||||
|
||||
@pytest.mark.parametrize("n", ["-n0", "-n1"])
|
||||
def test_unserializable_warning_details(self, pytester, n) -> None:
|
||||
def test_unserializable_warning_details(
|
||||
self, pytester: pytest.Pytester, n: str
|
||||
) -> None:
|
||||
"""Check that warnings with unserializable _WARNING_DETAILS are
|
||||
handled correctly (#379).
|
||||
"""
|
||||
@@ -1049,7 +1052,7 @@ class TestNodeFailure:
|
||||
|
||||
|
||||
@pytest.mark.parametrize("n", [0, 2])
|
||||
def test_worker_id_fixture(pytester, n) -> None:
|
||||
def test_worker_id_fixture(pytester: pytest.Pytester, n: int) -> None:
|
||||
import glob
|
||||
|
||||
f = pytester.makepyfile(
|
||||
@@ -1065,8 +1068,8 @@ def test_worker_id_fixture(pytester, n) -> None:
|
||||
result.stdout.fnmatch_lines("* 2 passed in *")
|
||||
worker_ids = set()
|
||||
for fname in glob.glob(str(pytester.path / "*.txt")):
|
||||
with open(fname) as f:
|
||||
worker_ids.add(f.read().strip())
|
||||
with open(fname) as fp:
|
||||
worker_ids.add(fp.read().strip())
|
||||
if n == 0:
|
||||
assert worker_ids == {"master"}
|
||||
else:
|
||||
@@ -1074,7 +1077,7 @@ def test_worker_id_fixture(pytester, n) -> None:
|
||||
|
||||
|
||||
@pytest.mark.parametrize("n", [0, 2])
|
||||
def test_testrun_uid_fixture(pytester, n) -> None:
|
||||
def test_testrun_uid_fixture(pytester: pytest.Pytester, n: int) -> None:
|
||||
import glob
|
||||
|
||||
f = pytester.makepyfile(
|
||||
@@ -1090,14 +1093,14 @@ def test_testrun_uid_fixture(pytester, n) -> None:
|
||||
result.stdout.fnmatch_lines("* 2 passed in *")
|
||||
testrun_uids = set()
|
||||
for fname in glob.glob(str(pytester.path / "*.txt")):
|
||||
with open(fname) as f:
|
||||
testrun_uids.add(f.read().strip())
|
||||
with open(fname) as fp:
|
||||
testrun_uids.add(fp.read().strip())
|
||||
assert len(testrun_uids) == 1
|
||||
assert len(testrun_uids.pop()) == 32
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tb", ["auto", "long", "short", "no", "line", "native"])
|
||||
def test_error_report_styles(pytester, tb) -> None:
|
||||
def test_error_report_styles(pytester: pytest.Pytester, tb: str) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
@@ -1111,7 +1114,7 @@ def test_error_report_styles(pytester, tb) -> None:
|
||||
result.assert_outcomes(failed=1)
|
||||
|
||||
|
||||
def test_color_yes_collection_on_non_atty(pytester) -> None:
|
||||
def test_color_yes_collection_on_non_atty(pytester: pytest.Pytester) -> None:
|
||||
"""Skip collect progress report when working on non-terminals.
|
||||
|
||||
Similar to pytest-dev/pytest#1397
|
||||
@@ -1133,7 +1136,7 @@ def test_color_yes_collection_on_non_atty(pytester) -> None:
|
||||
assert "collecting:" not in result.stdout.str()
|
||||
|
||||
|
||||
def test_without_terminal_plugin(pytester, request) -> None:
|
||||
def test_without_terminal_plugin(pytester: pytest.Pytester) -> None:
|
||||
"""No output when terminal plugin is disabled."""
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
@@ -1368,7 +1371,7 @@ class TestFileScope:
|
||||
|
||||
|
||||
class TestGroupScope:
|
||||
def test_by_module(self, pytester: pytest.Pytester):
|
||||
def test_by_module(self, pytester: pytest.Pytester) -> None:
|
||||
test_file = """
|
||||
import pytest
|
||||
class TestA:
|
||||
@@ -1399,7 +1402,7 @@ class TestGroupScope:
|
||||
== test_b_workers_and_test_count.items()
|
||||
)
|
||||
|
||||
def test_by_class(self, pytester: pytest.Pytester):
|
||||
def test_by_class(self, pytester: pytest.Pytester) -> None:
|
||||
pytester.makepyfile(
|
||||
test_a="""
|
||||
import pytest
|
||||
@@ -1436,7 +1439,7 @@ class TestGroupScope:
|
||||
== test_b_workers_and_test_count.items()
|
||||
)
|
||||
|
||||
def test_module_single_start(self, pytester: pytest.Pytester):
|
||||
def test_module_single_start(self, pytester: pytest.Pytester) -> None:
|
||||
test_file1 = """
|
||||
import pytest
|
||||
@pytest.mark.xdist_group(name="xdist_group")
|
||||
@@ -1459,7 +1462,7 @@ class TestGroupScope:
|
||||
|
||||
assert a.keys() == b.keys() and b.keys() == c.keys()
|
||||
|
||||
def test_with_two_group_names(self, pytester: pytest.Pytester):
|
||||
def test_with_two_group_names(self, pytester: pytest.Pytester) -> None:
|
||||
test_file = """
|
||||
import pytest
|
||||
@pytest.mark.xdist_group(name="group1")
|
||||
@@ -1512,7 +1515,7 @@ class TestLocking:
|
||||
@pytest.mark.parametrize(
|
||||
"scope", ["each", "load", "loadscope", "loadfile", "worksteal", "no"]
|
||||
)
|
||||
def test_single_file(self, pytester, scope) -> None:
|
||||
def test_single_file(self, pytester: pytest.Pytester, scope: str) -> None:
|
||||
pytester.makepyfile(test_a=self.test_file1)
|
||||
result = pytester.runpytest("-n2", "--dist=%s" % scope, "-v")
|
||||
result.assert_outcomes(passed=(12 if scope != "each" else 12 * 2))
|
||||
@@ -1520,7 +1523,7 @@ class TestLocking:
|
||||
@pytest.mark.parametrize(
|
||||
"scope", ["each", "load", "loadscope", "loadfile", "worksteal", "no"]
|
||||
)
|
||||
def test_multi_file(self, pytester, scope) -> None:
|
||||
def test_multi_file(self, pytester: pytest.Pytester, scope: str) -> None:
|
||||
pytester.makepyfile(
|
||||
test_a=self.test_file1,
|
||||
test_b=self.test_file1,
|
||||
@@ -1564,32 +1567,32 @@ def get_workers_and_test_count_by_prefix(
|
||||
|
||||
class TestAPI:
|
||||
@pytest.fixture
|
||||
def fake_request(self):
|
||||
def fake_request(self) -> pytest.FixtureRequest:
|
||||
class FakeOption:
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.dist = "load"
|
||||
|
||||
class FakeConfig:
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.workerinput = {"workerid": "gw5"}
|
||||
self.option = FakeOption()
|
||||
|
||||
class FakeRequest:
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.config = FakeConfig()
|
||||
|
||||
return FakeRequest()
|
||||
return cast(pytest.FixtureRequest, FakeRequest())
|
||||
|
||||
def test_is_xdist_worker(self, fake_request) -> None:
|
||||
def test_is_xdist_worker(self, fake_request: pytest.FixtureRequest) -> None:
|
||||
assert xdist.is_xdist_worker(fake_request)
|
||||
del fake_request.config.workerinput
|
||||
del fake_request.config.workerinput # type: ignore[attr-defined]
|
||||
assert not xdist.is_xdist_worker(fake_request)
|
||||
|
||||
def test_is_xdist_controller(self, fake_request) -> None:
|
||||
def test_is_xdist_controller(self, fake_request: pytest.FixtureRequest) -> None:
|
||||
assert not xdist.is_xdist_master(fake_request)
|
||||
assert not xdist.is_xdist_controller(fake_request)
|
||||
|
||||
del fake_request.config.workerinput
|
||||
del fake_request.config.workerinput # type: ignore[attr-defined]
|
||||
assert xdist.is_xdist_master(fake_request)
|
||||
assert xdist.is_xdist_controller(fake_request)
|
||||
|
||||
@@ -1597,13 +1600,13 @@ class TestAPI:
|
||||
assert not xdist.is_xdist_master(fake_request)
|
||||
assert not xdist.is_xdist_controller(fake_request)
|
||||
|
||||
def test_get_xdist_worker_id(self, fake_request) -> None:
|
||||
def test_get_xdist_worker_id(self, fake_request: pytest.FixtureRequest) -> None:
|
||||
assert xdist.get_xdist_worker_id(fake_request) == "gw5"
|
||||
del fake_request.config.workerinput
|
||||
del fake_request.config.workerinput # type: ignore[attr-defined]
|
||||
assert xdist.get_xdist_worker_id(fake_request) == "master"
|
||||
|
||||
|
||||
def test_collection_crash(pytester: pytest.Pytester):
|
||||
def test_collection_crash(pytester: pytest.Pytester) -> None:
|
||||
p1 = pytester.makepyfile(
|
||||
"""
|
||||
assert 0
|
||||
@@ -1622,7 +1625,7 @@ def test_collection_crash(pytester: pytest.Pytester):
|
||||
)
|
||||
|
||||
|
||||
def test_dist_in_addopts(pytester: pytest.Pytester):
|
||||
def test_dist_in_addopts(pytester: pytest.Pytester) -> None:
|
||||
"""Users can set a default distribution in the configuration file (#789)."""
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
from typing import Callable
|
||||
from typing import Generator
|
||||
|
||||
import execnet
|
||||
import pytest
|
||||
@@ -10,12 +12,14 @@ pytest_plugins = "pytester"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _divert_atexit(request, monkeypatch: pytest.MonkeyPatch):
|
||||
def _divert_atexit(monkeypatch: pytest.MonkeyPatch) -> Generator[None, None, None]:
|
||||
import atexit
|
||||
|
||||
finalizers = []
|
||||
|
||||
def fake_register(func, *args, **kwargs):
|
||||
def fake_register(
|
||||
func: Callable[..., object], *args: object, **kwargs: object
|
||||
) -> None:
|
||||
finalizers.append((func, args, kwargs))
|
||||
|
||||
monkeypatch.setattr(atexit, "register", fake_register)
|
||||
@@ -27,7 +31,7 @@ def _divert_atexit(request, monkeypatch: pytest.MonkeyPatch):
|
||||
func(*args, **kwargs)
|
||||
|
||||
|
||||
def pytest_addoption(parser) -> None:
|
||||
def pytest_addoption(parser: pytest.Parser) -> None:
|
||||
parser.addoption(
|
||||
"--gx",
|
||||
action="append",
|
||||
@@ -37,16 +41,16 @@ def pytest_addoption(parser) -> None:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def specssh(request) -> str:
|
||||
def specssh(request: pytest.FixtureRequest) -> str:
|
||||
return getspecssh(request.config)
|
||||
|
||||
|
||||
# configuration information for tests
|
||||
def getgspecs(config) -> list[execnet.XSpec]:
|
||||
def getgspecs(config: pytest.Config) -> list[execnet.XSpec]:
|
||||
return [execnet.XSpec(spec) for spec in config.getvalueorskip("gspecs")]
|
||||
|
||||
|
||||
def getspecssh(config) -> str: # type: ignore[return]
|
||||
def getspecssh(config: pytest.Config) -> str:
|
||||
xspecs = getgspecs(config)
|
||||
for spec in xspecs:
|
||||
if spec.ssh:
|
||||
@@ -56,7 +60,7 @@ def getspecssh(config) -> str: # type: ignore[return]
|
||||
pytest.skip("need '--gx ssh=...'")
|
||||
|
||||
|
||||
def getsocketspec(config) -> execnet.XSpec:
|
||||
def getsocketspec(config: pytest.Config) -> execnet.XSpec:
|
||||
xspecs = getgspecs(config)
|
||||
for spec in xspecs:
|
||||
if spec.socket:
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
from typing import Sequence
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import execnet
|
||||
import pytest
|
||||
@@ -13,29 +16,38 @@ from xdist.report import report_collection_diff
|
||||
from xdist.scheduler import EachScheduling
|
||||
from xdist.scheduler import LoadScheduling
|
||||
from xdist.scheduler import WorkStealingScheduling
|
||||
from xdist.workermanage import WorkerController
|
||||
|
||||
|
||||
class MockGateway:
|
||||
if TYPE_CHECKING:
|
||||
BaseOfMockGateway = execnet.Gateway
|
||||
BaseOfMockNode = WorkerController
|
||||
else:
|
||||
BaseOfMockGateway = object
|
||||
BaseOfMockNode = object
|
||||
|
||||
|
||||
class MockGateway(BaseOfMockGateway):
|
||||
def __init__(self) -> None:
|
||||
self._count = 0
|
||||
self.id = str(self._count)
|
||||
self._count += 1
|
||||
|
||||
|
||||
class MockNode:
|
||||
class MockNode(BaseOfMockNode):
|
||||
def __init__(self) -> None:
|
||||
self.sent = [] # type: ignore[var-annotated]
|
||||
self.stolen = [] # type: ignore[var-annotated]
|
||||
self.sent: list[int | str] = []
|
||||
self.stolen: list[int] = []
|
||||
self.gateway = MockGateway()
|
||||
self._shutdown = False
|
||||
|
||||
def send_runtest_some(self, indices) -> None:
|
||||
def send_runtest_some(self, indices: Sequence[int]) -> None:
|
||||
self.sent.extend(indices)
|
||||
|
||||
def send_runtest_all(self) -> None:
|
||||
self.sent.append("ALL")
|
||||
|
||||
def send_steal(self, indices) -> None:
|
||||
def send_steal(self, indices: Sequence[int]) -> None:
|
||||
self.stolen.extend(indices)
|
||||
|
||||
def shutdown(self) -> None:
|
||||
@@ -48,10 +60,9 @@ class MockNode:
|
||||
|
||||
class TestEachScheduling:
|
||||
def test_schedule_load_simple(self, pytester: pytest.Pytester) -> None:
|
||||
node1 = MockNode()
|
||||
node2 = MockNode()
|
||||
config = pytester.parseconfig("--tx=2*popen")
|
||||
sched = EachScheduling(config)
|
||||
node1, node2 = MockNode(), MockNode()
|
||||
sched.add_node(node1)
|
||||
sched.add_node(node2)
|
||||
collection = ["a.py::test_1"]
|
||||
@@ -59,7 +70,7 @@ class TestEachScheduling:
|
||||
sched.add_node_collection(node1, collection)
|
||||
assert not sched.collection_is_completed
|
||||
sched.add_node_collection(node2, collection)
|
||||
assert sched.collection_is_completed
|
||||
assert bool(sched.collection_is_completed)
|
||||
assert sched.node2collection[node1] == collection
|
||||
assert sched.node2collection[node2] == collection
|
||||
sched.schedule()
|
||||
@@ -72,14 +83,14 @@ class TestEachScheduling:
|
||||
assert sched.tests_finished
|
||||
|
||||
def test_schedule_remove_node(self, pytester: pytest.Pytester) -> None:
|
||||
node1 = MockNode()
|
||||
config = pytester.parseconfig("--tx=popen")
|
||||
sched = EachScheduling(config)
|
||||
node1 = MockNode()
|
||||
sched.add_node(node1)
|
||||
collection = ["a.py::test_1"]
|
||||
assert not sched.collection_is_completed
|
||||
sched.add_node_collection(node1, collection)
|
||||
assert sched.collection_is_completed
|
||||
assert bool(sched.collection_is_completed)
|
||||
assert sched.node2collection[node1] == collection
|
||||
sched.schedule()
|
||||
assert sched.tests_finished
|
||||
@@ -93,15 +104,15 @@ class TestLoadScheduling:
|
||||
def test_schedule_load_simple(self, pytester: pytest.Pytester) -> None:
|
||||
config = pytester.parseconfig("--tx=2*popen")
|
||||
sched = LoadScheduling(config)
|
||||
sched.add_node(MockNode())
|
||||
sched.add_node(MockNode())
|
||||
node1, node2 = sched.nodes
|
||||
node1, node2 = MockNode(), MockNode()
|
||||
sched.add_node(node1)
|
||||
sched.add_node(node2)
|
||||
collection = ["a.py::test_1", "a.py::test_2"]
|
||||
assert not sched.collection_is_completed
|
||||
sched.add_node_collection(node1, collection)
|
||||
assert not sched.collection_is_completed
|
||||
sched.add_node_collection(node2, collection)
|
||||
assert sched.collection_is_completed
|
||||
assert bool(sched.collection_is_completed)
|
||||
assert sched.node2collection[node1] == collection
|
||||
assert sched.node2collection[node2] == collection
|
||||
sched.schedule()
|
||||
@@ -111,15 +122,17 @@ class TestLoadScheduling:
|
||||
assert len(node2.sent) == 1
|
||||
assert node1.sent == [0]
|
||||
assert node2.sent == [1]
|
||||
sched.mark_test_complete(node1, node1.sent[0])
|
||||
sent10 = node1.sent[0]
|
||||
assert isinstance(sent10, int)
|
||||
sched.mark_test_complete(node1, sent10)
|
||||
assert sched.tests_finished
|
||||
|
||||
def test_schedule_batch_size(self, pytester: pytest.Pytester) -> None:
|
||||
config = pytester.parseconfig("--tx=2*popen")
|
||||
sched = LoadScheduling(config)
|
||||
sched.add_node(MockNode())
|
||||
sched.add_node(MockNode())
|
||||
node1, node2 = sched.nodes
|
||||
node1, node2 = MockNode(), MockNode()
|
||||
sched.add_node(node1)
|
||||
sched.add_node(node2)
|
||||
col = ["xyz"] * 6
|
||||
sched.add_node_collection(node1, col)
|
||||
sched.add_node_collection(node2, col)
|
||||
@@ -144,9 +157,9 @@ class TestLoadScheduling:
|
||||
def test_schedule_maxchunk_none(self, pytester: pytest.Pytester) -> None:
|
||||
config = pytester.parseconfig("--tx=2*popen")
|
||||
sched = LoadScheduling(config)
|
||||
sched.add_node(MockNode())
|
||||
sched.add_node(MockNode())
|
||||
node1, node2 = sched.nodes
|
||||
node1, node2 = MockNode(), MockNode()
|
||||
sched.add_node(node1)
|
||||
sched.add_node(node2)
|
||||
col = [f"test{i}" for i in range(16)]
|
||||
sched.add_node_collection(node1, col)
|
||||
sched.add_node_collection(node2, col)
|
||||
@@ -172,9 +185,9 @@ class TestLoadScheduling:
|
||||
def test_schedule_maxchunk_1(self, pytester: pytest.Pytester) -> None:
|
||||
config = pytester.parseconfig("--tx=2*popen", "--maxschedchunk=1")
|
||||
sched = LoadScheduling(config)
|
||||
sched.add_node(MockNode())
|
||||
sched.add_node(MockNode())
|
||||
node1, node2 = sched.nodes
|
||||
node1, node2 = MockNode(), MockNode()
|
||||
sched.add_node(node1)
|
||||
sched.add_node(node2)
|
||||
col = [f"test{i}" for i in range(16)]
|
||||
sched.add_node_collection(node1, col)
|
||||
sched.add_node_collection(node2, col)
|
||||
@@ -186,7 +199,9 @@ class TestLoadScheduling:
|
||||
assert sched.node2pending[node2] == node2.sent
|
||||
|
||||
for complete_index, first_pending in enumerate(range(5, 16)):
|
||||
sched.mark_test_complete(node1, node1.sent[complete_index])
|
||||
sent_index = node1.sent[complete_index]
|
||||
assert isinstance(sent_index, int)
|
||||
sched.mark_test_complete(node1, sent_index)
|
||||
assert node1.sent == [0, 1, *range(4, first_pending)]
|
||||
assert node2.sent == [2, 3]
|
||||
assert sched.pending == list(range(first_pending, 16))
|
||||
@@ -194,10 +209,10 @@ class TestLoadScheduling:
|
||||
def test_schedule_fewer_tests_than_nodes(self, pytester: pytest.Pytester) -> None:
|
||||
config = pytester.parseconfig("--tx=3*popen")
|
||||
sched = LoadScheduling(config)
|
||||
sched.add_node(MockNode())
|
||||
sched.add_node(MockNode())
|
||||
sched.add_node(MockNode())
|
||||
node1, node2, node3 = sched.nodes
|
||||
node1, node2, node3 = MockNode(), MockNode(), MockNode()
|
||||
sched.add_node(node1)
|
||||
sched.add_node(node2)
|
||||
sched.add_node(node3)
|
||||
col = ["xyz"] * 2
|
||||
sched.add_node_collection(node1, col)
|
||||
sched.add_node_collection(node2, col)
|
||||
@@ -215,10 +230,10 @@ class TestLoadScheduling:
|
||||
) -> None:
|
||||
config = pytester.parseconfig("--tx=3*popen")
|
||||
sched = LoadScheduling(config)
|
||||
sched.add_node(MockNode())
|
||||
sched.add_node(MockNode())
|
||||
sched.add_node(MockNode())
|
||||
node1, node2, node3 = sched.nodes
|
||||
node1, node2, node3 = MockNode(), MockNode(), MockNode()
|
||||
sched.add_node(node1)
|
||||
sched.add_node(node2)
|
||||
sched.add_node(node3)
|
||||
col = ["xyz"] * 5
|
||||
sched.add_node_collection(node1, col)
|
||||
sched.add_node_collection(node2, col)
|
||||
@@ -232,9 +247,9 @@ class TestLoadScheduling:
|
||||
assert not sched.pending
|
||||
|
||||
def test_add_remove_node(self, pytester: pytest.Pytester) -> None:
|
||||
node = MockNode()
|
||||
config = pytester.parseconfig("--tx=popen")
|
||||
sched = LoadScheduling(config)
|
||||
node = MockNode()
|
||||
sched.add_node(node)
|
||||
collection = ["test_file.py::test_func"]
|
||||
sched.add_node_collection(node, collection)
|
||||
@@ -253,18 +268,17 @@ class TestLoadScheduling:
|
||||
class CollectHook:
|
||||
"""Dummy hook that stores collection reports."""
|
||||
|
||||
def __init__(self):
|
||||
self.reports = []
|
||||
def __init__(self) -> None:
|
||||
self.reports: list[pytest.CollectReport] = []
|
||||
|
||||
def pytest_collectreport(self, report):
|
||||
def pytest_collectreport(self, report: pytest.CollectReport) -> None:
|
||||
self.reports.append(report)
|
||||
|
||||
collect_hook = CollectHook()
|
||||
config = pytester.parseconfig("--tx=2*popen")
|
||||
config.pluginmanager.register(collect_hook, "collect_hook")
|
||||
node1 = MockNode()
|
||||
node2 = MockNode()
|
||||
sched = LoadScheduling(config)
|
||||
node1, node2 = MockNode(), MockNode()
|
||||
sched.add_node(node1)
|
||||
sched.add_node(node2)
|
||||
sched.add_node_collection(node1, ["a.py::test_1"])
|
||||
@@ -272,6 +286,7 @@ class TestLoadScheduling:
|
||||
sched.schedule()
|
||||
assert len(collect_hook.reports) == 1
|
||||
rep = collect_hook.reports[0]
|
||||
assert isinstance(rep.longrepr, str)
|
||||
assert "Different tests were collected between" in rep.longrepr
|
||||
|
||||
|
||||
@@ -279,15 +294,15 @@ class TestWorkStealingScheduling:
|
||||
def test_ideal_case(self, pytester: pytest.Pytester) -> None:
|
||||
config = pytester.parseconfig("--tx=2*popen")
|
||||
sched = WorkStealingScheduling(config)
|
||||
sched.add_node(MockNode())
|
||||
sched.add_node(MockNode())
|
||||
node1, node2 = sched.nodes
|
||||
node1, node2 = MockNode(), MockNode()
|
||||
sched.add_node(node1)
|
||||
sched.add_node(node2)
|
||||
collection = [f"test_workstealing.py::test_{i}" for i in range(16)]
|
||||
assert not sched.collection_is_completed
|
||||
sched.add_node_collection(node1, collection)
|
||||
assert not sched.collection_is_completed
|
||||
sched.add_node_collection(node2, collection)
|
||||
assert sched.collection_is_completed
|
||||
assert bool(sched.collection_is_completed)
|
||||
assert sched.node2collection[node1] == collection
|
||||
assert sched.node2collection[node2] == collection
|
||||
sched.schedule()
|
||||
@@ -296,18 +311,20 @@ class TestWorkStealingScheduling:
|
||||
assert node1.sent == list(range(8))
|
||||
assert node2.sent == list(range(8, 16))
|
||||
for i in range(8):
|
||||
sched.mark_test_complete(node1, node1.sent[i])
|
||||
sched.mark_test_complete(node2, node2.sent[i])
|
||||
assert sched.tests_finished
|
||||
sent1, sent2 = node1.sent[i], node2.sent[i]
|
||||
assert isinstance(sent1, int) and isinstance(sent2, int)
|
||||
sched.mark_test_complete(node1, sent1)
|
||||
sched.mark_test_complete(node2, sent2)
|
||||
assert bool(sched.tests_finished)
|
||||
assert node1.stolen == []
|
||||
assert node2.stolen == []
|
||||
|
||||
def test_stealing(self, pytester: pytest.Pytester) -> None:
|
||||
config = pytester.parseconfig("--tx=2*popen")
|
||||
sched = WorkStealingScheduling(config)
|
||||
sched.add_node(MockNode())
|
||||
sched.add_node(MockNode())
|
||||
node1, node2 = sched.nodes
|
||||
node1, node2 = MockNode(), MockNode()
|
||||
sched.add_node(node1)
|
||||
sched.add_node(node2)
|
||||
collection = [f"test_workstealing.py::test_{i}" for i in range(16)]
|
||||
sched.add_node_collection(node1, collection)
|
||||
sched.add_node_collection(node2, collection)
|
||||
@@ -316,11 +333,15 @@ class TestWorkStealingScheduling:
|
||||
assert node1.sent == list(range(8))
|
||||
assert node2.sent == list(range(8, 16))
|
||||
for i in range(8):
|
||||
sched.mark_test_complete(node1, node1.sent[i])
|
||||
sent = node1.sent[i]
|
||||
assert isinstance(sent, int)
|
||||
sched.mark_test_complete(node1, sent)
|
||||
assert node2.stolen == list(range(12, 16))
|
||||
sched.remove_pending_tests_from_node(node2, node2.stolen)
|
||||
for i in range(4):
|
||||
sched.mark_test_complete(node2, node2.sent[i])
|
||||
sent = node2.sent[i]
|
||||
assert isinstance(sent, int)
|
||||
sched.mark_test_complete(node2, sent)
|
||||
assert node1.stolen == [14, 15]
|
||||
sched.remove_pending_tests_from_node(node1, node1.stolen)
|
||||
sched.mark_test_complete(node1, 12)
|
||||
@@ -355,10 +376,10 @@ class TestWorkStealingScheduling:
|
||||
def test_schedule_fewer_tests_than_nodes(self, pytester: pytest.Pytester) -> None:
|
||||
config = pytester.parseconfig("--tx=3*popen")
|
||||
sched = WorkStealingScheduling(config)
|
||||
sched.add_node(MockNode())
|
||||
sched.add_node(MockNode())
|
||||
sched.add_node(MockNode())
|
||||
node1, node2, node3 = sched.nodes
|
||||
node1, node2, node3 = MockNode(), MockNode(), MockNode()
|
||||
sched.add_node(node1)
|
||||
sched.add_node(node2)
|
||||
sched.add_node(node3)
|
||||
col = ["xyz"] * 2
|
||||
sched.add_node_collection(node1, col)
|
||||
sched.add_node_collection(node2, col)
|
||||
@@ -378,10 +399,10 @@ class TestWorkStealingScheduling:
|
||||
) -> None:
|
||||
config = pytester.parseconfig("--tx=3*popen")
|
||||
sched = WorkStealingScheduling(config)
|
||||
sched.add_node(MockNode())
|
||||
sched.add_node(MockNode())
|
||||
sched.add_node(MockNode())
|
||||
node1, node2, node3 = sched.nodes
|
||||
node1, node2, node3 = MockNode(), MockNode(), MockNode()
|
||||
sched.add_node(node1)
|
||||
sched.add_node(node2)
|
||||
sched.add_node(node3)
|
||||
col = ["xyz"] * 5
|
||||
sched.add_node_collection(node1, col)
|
||||
sched.add_node_collection(node2, col)
|
||||
@@ -392,11 +413,19 @@ class TestWorkStealingScheduling:
|
||||
assert node3.sent == [3, 4]
|
||||
assert not sched.pending
|
||||
assert not sched.tests_finished
|
||||
sched.mark_test_complete(node1, node1.sent[0])
|
||||
sched.mark_test_complete(node2, node2.sent[0])
|
||||
sched.mark_test_complete(node3, node3.sent[0])
|
||||
sched.mark_test_complete(node3, node3.sent[1])
|
||||
assert sched.tests_finished
|
||||
sent10 = node1.sent[0]
|
||||
assert isinstance(sent10, int)
|
||||
sent20 = node2.sent[0]
|
||||
assert isinstance(sent20, int)
|
||||
sent30 = node3.sent[0]
|
||||
assert isinstance(sent30, int)
|
||||
sent31 = node3.sent[1]
|
||||
assert isinstance(sent31, int)
|
||||
sched.mark_test_complete(node1, sent10)
|
||||
sched.mark_test_complete(node2, sent20)
|
||||
sched.mark_test_complete(node3, sent30)
|
||||
sched.mark_test_complete(node3, sent31)
|
||||
assert bool(sched.tests_finished)
|
||||
assert node1.stolen == []
|
||||
assert node2.stolen == []
|
||||
assert node3.stolen == []
|
||||
@@ -416,18 +445,17 @@ class TestWorkStealingScheduling:
|
||||
|
||||
def test_different_tests_collected(self, pytester: pytest.Pytester) -> None:
|
||||
class CollectHook:
|
||||
def __init__(self):
|
||||
self.reports = []
|
||||
def __init__(self) -> None:
|
||||
self.reports: list[pytest.CollectReport] = []
|
||||
|
||||
def pytest_collectreport(self, report):
|
||||
def pytest_collectreport(self, report: pytest.CollectReport) -> None:
|
||||
self.reports.append(report)
|
||||
|
||||
collect_hook = CollectHook()
|
||||
config = pytester.parseconfig("--tx=2*popen")
|
||||
config.pluginmanager.register(collect_hook, "collect_hook")
|
||||
node1 = MockNode()
|
||||
node2 = MockNode()
|
||||
sched = WorkStealingScheduling(config)
|
||||
node1, node2 = MockNode(), MockNode()
|
||||
sched.add_node(node1)
|
||||
sched.add_node(node2)
|
||||
sched.add_node_collection(node1, ["a.py::test_1"])
|
||||
@@ -435,12 +463,13 @@ class TestWorkStealingScheduling:
|
||||
sched.schedule()
|
||||
assert len(collect_hook.reports) == 1
|
||||
rep = collect_hook.reports[0]
|
||||
assert isinstance(rep.longrepr, str)
|
||||
assert "Different tests were collected between" in rep.longrepr
|
||||
|
||||
|
||||
class TestDistReporter:
|
||||
@pytest.mark.xfail
|
||||
def test_rsync_printing(self, pytester: pytest.Pytester, linecomp) -> None:
|
||||
def test_rsync_printing(self, pytester: pytest.Pytester, linecomp: Any) -> None:
|
||||
config = pytester.parseconfig()
|
||||
from _pytest.terminal import TerminalReporter
|
||||
|
||||
@@ -473,15 +502,17 @@ class TestDistReporter:
|
||||
def test_report_collection_diff_equal() -> None:
|
||||
"""Test reporting of equal collections."""
|
||||
from_collection = to_collection = ["aaa", "bbb", "ccc"]
|
||||
assert report_collection_diff(from_collection, to_collection, 1, 2) is None
|
||||
assert report_collection_diff(from_collection, to_collection, "1", "2") is None
|
||||
|
||||
|
||||
def test_default_max_worker_restart() -> None:
|
||||
class config:
|
||||
class MockConfig:
|
||||
class option:
|
||||
maxworkerrestart: str | None = None
|
||||
numprocesses: int = 0
|
||||
|
||||
config = cast(pytest.Config, MockConfig)
|
||||
|
||||
assert get_default_max_worker_restart(config) is None
|
||||
|
||||
config.option.numprocesses = 2
|
||||
|
||||
@@ -143,7 +143,7 @@ class TestRemoteControl:
|
||||
control = RemoteControl(modcol.config)
|
||||
control.loop_once()
|
||||
assert control.failures
|
||||
modcol_path = modcol.path # type:ignore[attr-defined]
|
||||
modcol_path = modcol.path
|
||||
|
||||
modcol_path.write_text(
|
||||
textwrap.dedent(
|
||||
@@ -173,7 +173,7 @@ class TestRemoteControl:
|
||||
"""
|
||||
)
|
||||
)
|
||||
parent = modcol.path.parent.parent # type: ignore[attr-defined]
|
||||
parent = modcol.path.parent.parent
|
||||
monkeypatch.chdir(parent)
|
||||
modcol.config.args = [
|
||||
str(Path(x).relative_to(parent)) for x in modcol.config.args
|
||||
@@ -332,7 +332,7 @@ class TestLooponFailing:
|
||||
remotecontrol = RemoteControl(modcol.config)
|
||||
orig_runsession = remotecontrol.runsession
|
||||
|
||||
def runsession_dups():
|
||||
def runsession_dups() -> tuple[list[str], list[str], bool]:
|
||||
# twisted.trial test cases may report multiple errors.
|
||||
failures, reports, collection_failed = orig_runsession()
|
||||
print(failures)
|
||||
|
||||
@@ -10,7 +10,7 @@ from xdist.workermanage import NodeManager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def monkeypatch_3_cpus(monkeypatch: pytest.MonkeyPatch):
|
||||
def monkeypatch_3_cpus(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Make pytest-xdist believe the system has 3 CPUs."""
|
||||
# block import
|
||||
monkeypatch.setitem(sys.modules, "psutil", None)
|
||||
@@ -128,7 +128,7 @@ def test_auto_detect_cpus_psutil(
|
||||
|
||||
|
||||
def test_auto_detect_cpus_os(
|
||||
pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch, monkeypatch_3_cpus
|
||||
pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch, monkeypatch_3_cpus: None
|
||||
) -> None:
|
||||
from xdist.plugin import pytest_cmdline_main as check_options
|
||||
|
||||
@@ -189,7 +189,7 @@ def test_hook_auto_num_workers_arg(
|
||||
|
||||
|
||||
def test_hook_auto_num_workers_none(
|
||||
pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch, monkeypatch_3_cpus
|
||||
pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch, monkeypatch_3_cpus: None
|
||||
) -> None:
|
||||
# Returning None from a hook to skip it is pytest behavior,
|
||||
# but we document it so let's test it.
|
||||
@@ -231,7 +231,7 @@ def test_envvar_auto_num_workers(
|
||||
|
||||
|
||||
def test_envvar_auto_num_workers_warn(
|
||||
pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch, monkeypatch_3_cpus
|
||||
pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch, monkeypatch_3_cpus: None
|
||||
) -> None:
|
||||
from xdist.plugin import pytest_cmdline_main as check_options
|
||||
|
||||
@@ -244,7 +244,7 @@ def test_envvar_auto_num_workers_warn(
|
||||
|
||||
|
||||
def test_auto_num_workers_hook_overrides_envvar(
|
||||
pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch, monkeypatch_3_cpus
|
||||
pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch, monkeypatch_3_cpus: None
|
||||
) -> None:
|
||||
from xdist.plugin import pytest_cmdline_main as check_options
|
||||
|
||||
|
||||
@@ -1,36 +1,46 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import marshal
|
||||
import pprint
|
||||
from queue import Queue
|
||||
import sys
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Dict
|
||||
from typing import Union
|
||||
import uuid
|
||||
|
||||
import execnet
|
||||
import pytest
|
||||
|
||||
from xdist.workermanage import NodeManager
|
||||
from xdist.workermanage import WorkerController
|
||||
|
||||
|
||||
WAIT_TIMEOUT = 10.0
|
||||
|
||||
|
||||
def check_marshallable(d):
|
||||
def check_marshallable(d: object) -> None:
|
||||
try:
|
||||
marshal.dumps(d)
|
||||
marshal.dumps(d) # type: ignore[arg-type]
|
||||
except ValueError as e:
|
||||
pprint.pprint(d)
|
||||
raise ValueError("not marshallable") from e
|
||||
|
||||
|
||||
class EventCall:
|
||||
def __init__(self, eventcall):
|
||||
def __init__(self, eventcall: tuple[str, dict[str, Any]]) -> None:
|
||||
self.name, self.kwargs = eventcall
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f"<EventCall {self.name}(**{self.kwargs})>"
|
||||
|
||||
|
||||
class WorkerSetup:
|
||||
def __init__(self, request, pytester: pytest.Pytester) -> None:
|
||||
def __init__(
|
||||
self, request: pytest.FixtureRequest, pytester: pytest.Pytester
|
||||
) -> None:
|
||||
self.request = request
|
||||
self.pytester = pytester
|
||||
self.use_callback = False
|
||||
@@ -47,11 +57,18 @@ class WorkerSetup:
|
||||
testrunuid = uuid.uuid4().hex
|
||||
specs = [0, 1]
|
||||
|
||||
self.slp = WorkerController(DummyMananger, self.gateway, config, putevent)
|
||||
nodemanager = cast(NodeManager, DummyMananger)
|
||||
|
||||
self.slp = WorkerController(
|
||||
nodemanager=nodemanager,
|
||||
gateway=self.gateway,
|
||||
config=config,
|
||||
putevent=putevent, # type: ignore[arg-type]
|
||||
)
|
||||
self.request.addfinalizer(self.slp.ensure_teardown)
|
||||
self.slp.setup()
|
||||
|
||||
def popevent(self, name=None):
|
||||
def popevent(self, name: str | None = None) -> EventCall:
|
||||
while 1:
|
||||
if self.use_callback:
|
||||
data = self.events.get(timeout=WAIT_TIMEOUT)
|
||||
@@ -62,27 +79,33 @@ class WorkerSetup:
|
||||
return ev
|
||||
print(f"skipping {ev}")
|
||||
|
||||
def sendcommand(self, name, **kwargs):
|
||||
def sendcommand(self, name: str, **kwargs: Any) -> None:
|
||||
self.slp.sendcommand(name, **kwargs)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def worker(request, pytester: pytest.Pytester) -> WorkerSetup:
|
||||
def worker(request: pytest.FixtureRequest, pytester: pytest.Pytester) -> WorkerSetup:
|
||||
return WorkerSetup(request, pytester)
|
||||
|
||||
|
||||
class TestWorkerInteractor:
|
||||
UnserializerReport = Callable[
|
||||
[Dict[str, Any]], Union[pytest.CollectReport, pytest.TestReport]
|
||||
]
|
||||
|
||||
@pytest.fixture
|
||||
def unserialize_report(self, pytestconfig):
|
||||
def unserialize(data):
|
||||
return pytestconfig.hook.pytest_report_from_serializable(
|
||||
def unserialize_report(self, pytestconfig: pytest.Config) -> UnserializerReport:
|
||||
def unserialize(
|
||||
data: dict[str, Any],
|
||||
) -> pytest.CollectReport | pytest.TestReport:
|
||||
return pytestconfig.hook.pytest_report_from_serializable( # type: ignore[no-any-return]
|
||||
config=pytestconfig, data=data
|
||||
)
|
||||
|
||||
return unserialize
|
||||
|
||||
def test_basic_collect_and_runtests(
|
||||
self, worker: WorkerSetup, unserialize_report
|
||||
self, worker: WorkerSetup, unserialize_report: UnserializerReport
|
||||
) -> None:
|
||||
worker.pytester.makepyfile(
|
||||
"""
|
||||
@@ -115,7 +138,9 @@ class TestWorkerInteractor:
|
||||
ev = worker.popevent("workerfinished")
|
||||
assert "workeroutput" in ev.kwargs
|
||||
|
||||
def test_remote_collect_skip(self, worker: WorkerSetup, unserialize_report) -> None:
|
||||
def test_remote_collect_skip(
|
||||
self, worker: WorkerSetup, unserialize_report: UnserializerReport
|
||||
) -> None:
|
||||
worker.pytester.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
@@ -129,11 +154,14 @@ class TestWorkerInteractor:
|
||||
assert ev.name == "collectreport"
|
||||
rep = unserialize_report(ev.kwargs["data"])
|
||||
assert rep.skipped
|
||||
assert isinstance(rep.longrepr, tuple)
|
||||
assert rep.longrepr[2] == "Skipped: hello"
|
||||
ev = worker.popevent("collectionfinish")
|
||||
assert not ev.kwargs["ids"]
|
||||
|
||||
def test_remote_collect_fail(self, worker: WorkerSetup, unserialize_report) -> None:
|
||||
def test_remote_collect_fail(
|
||||
self, worker: WorkerSetup, unserialize_report: UnserializerReport
|
||||
) -> None:
|
||||
worker.pytester.makepyfile("""aasd qwe""")
|
||||
worker.setup()
|
||||
ev = worker.popevent("collectionstart")
|
||||
@@ -145,7 +173,9 @@ class TestWorkerInteractor:
|
||||
ev = worker.popevent("collectionfinish")
|
||||
assert not ev.kwargs["ids"]
|
||||
|
||||
def test_runtests_all(self, worker: WorkerSetup, unserialize_report) -> None:
|
||||
def test_runtests_all(
|
||||
self, worker: WorkerSetup, unserialize_report: UnserializerReport
|
||||
) -> None:
|
||||
worker.pytester.makepyfile(
|
||||
"""
|
||||
def test_func(): pass
|
||||
@@ -205,13 +235,15 @@ class TestWorkerInteractor:
|
||||
) -> None:
|
||||
worker.use_callback = True
|
||||
worker.setup()
|
||||
worker.slp.process_from_remote(("<nonono>", ()))
|
||||
worker.slp.process_from_remote(("<nonono>", {}))
|
||||
out, err = capsys.readouterr()
|
||||
assert "INTERNALERROR> ValueError: unknown event: <nonono>" in out
|
||||
ev = worker.popevent()
|
||||
assert ev.name == "errordown"
|
||||
|
||||
def test_steal_work(self, worker: WorkerSetup, unserialize_report) -> None:
|
||||
def test_steal_work(
|
||||
self, worker: WorkerSetup, unserialize_report: UnserializerReport
|
||||
) -> None:
|
||||
worker.pytester.makepyfile(
|
||||
"""
|
||||
import time
|
||||
@@ -262,7 +294,9 @@ class TestWorkerInteractor:
|
||||
ev = worker.popevent("workerfinished")
|
||||
assert "workeroutput" in ev.kwargs
|
||||
|
||||
def test_steal_empty_queue(self, worker: WorkerSetup, unserialize_report) -> None:
|
||||
def test_steal_empty_queue(
|
||||
self, worker: WorkerSetup, unserialize_report: UnserializerReport
|
||||
) -> None:
|
||||
worker.pytester.makepyfile(
|
||||
"""
|
||||
def test_func(): pass
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import textwrap
|
||||
@@ -19,13 +21,15 @@ pytest_plugins = "pytester"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hookrecorder(request, config, pytester: pytest.Pytester):
|
||||
def hookrecorder(
|
||||
config: pytest.Config, pytester: pytest.Pytester
|
||||
) -> pytest.HookRecorder:
|
||||
hookrecorder = pytester.make_hook_recorder(config.pluginmanager)
|
||||
return hookrecorder
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config(pytester: pytest.Pytester):
|
||||
def config(pytester: pytest.Pytester) -> pytest.Config:
|
||||
return pytester.parseconfig()
|
||||
|
||||
|
||||
@@ -44,24 +48,23 @@ def dest(tmp_path: Path) -> Path:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def workercontroller(monkeypatch: pytest.MonkeyPatch):
|
||||
def workercontroller(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
class MockController:
|
||||
def __init__(self, *args):
|
||||
def __init__(self, *args: object) -> None:
|
||||
pass
|
||||
|
||||
def setup(self):
|
||||
def setup(self) -> None:
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(workermanage, "WorkerController", MockController)
|
||||
return MockController
|
||||
|
||||
|
||||
class TestNodeManagerPopen:
|
||||
def test_popen_no_default_chdir(self, config) -> None:
|
||||
def test_popen_no_default_chdir(self, config: pytest.Config) -> None:
|
||||
gm = NodeManager(config, ["popen"])
|
||||
assert gm.specs[0].chdir is None
|
||||
|
||||
def test_default_chdir(self, config) -> None:
|
||||
def test_default_chdir(self, config: pytest.Config) -> None:
|
||||
specs = ["ssh=noco", "socket=xyz"]
|
||||
for spec in NodeManager(config, specs).specs:
|
||||
assert spec.chdir == "pyexecnetcache"
|
||||
@@ -69,10 +72,13 @@ class TestNodeManagerPopen:
|
||||
assert spec.chdir == "abc"
|
||||
|
||||
def test_popen_makegateway_events(
|
||||
self, config, hookrecorder, workercontroller
|
||||
self,
|
||||
config: pytest.Config,
|
||||
hookrecorder: pytest.HookRecorder,
|
||||
workercontroller: None,
|
||||
) -> None:
|
||||
hm = NodeManager(config, ["popen"] * 2)
|
||||
hm.setup_nodes(None)
|
||||
hm.setup_nodes(None) # type: ignore[arg-type]
|
||||
call = hookrecorder.popcall("pytest_xdist_setupnodes")
|
||||
assert len(call.specs) == 2
|
||||
|
||||
@@ -86,20 +92,24 @@ class TestNodeManagerPopen:
|
||||
assert not len(hm.group)
|
||||
|
||||
def test_popens_rsync(
|
||||
self, config, source: Path, dest: Path, workercontroller
|
||||
self,
|
||||
config: pytest.Config,
|
||||
source: Path,
|
||||
dest: Path,
|
||||
workercontroller: None,
|
||||
) -> None:
|
||||
hm = NodeManager(config, ["popen"] * 2)
|
||||
hm.setup_nodes(None)
|
||||
hm.setup_nodes(None) # type: ignore[arg-type]
|
||||
assert len(hm.group) == 2
|
||||
for gw in hm.group:
|
||||
|
||||
class pseudoexec:
|
||||
args = [] # type: ignore[var-annotated]
|
||||
|
||||
def __init__(self, *args):
|
||||
def __init__(self, *args: object) -> None:
|
||||
self.args.extend(args)
|
||||
|
||||
def waitclose(self):
|
||||
def waitclose(self) -> None:
|
||||
pass
|
||||
|
||||
gw.remote_exec = pseudoexec # type: ignore[assignment]
|
||||
@@ -112,10 +122,10 @@ class TestNodeManagerPopen:
|
||||
assert "sys.path.insert" in gw.remote_exec.args[0] # type: ignore[attr-defined]
|
||||
|
||||
def test_rsync_popen_with_path(
|
||||
self, config, source: Path, dest: Path, workercontroller
|
||||
self, config: pytest.Config, source: Path, dest: Path, workercontroller: None
|
||||
) -> None:
|
||||
hm = NodeManager(config, ["popen//chdir=%s" % dest] * 1)
|
||||
hm.setup_nodes(None)
|
||||
hm.setup_nodes(None) # type: ignore[arg-type]
|
||||
source.joinpath("dir1", "dir2").mkdir(parents=True)
|
||||
source.joinpath("dir1", "dir2", "hello").touch()
|
||||
notifications = []
|
||||
@@ -131,15 +141,15 @@ class TestNodeManagerPopen:
|
||||
|
||||
def test_rsync_same_popen_twice(
|
||||
self,
|
||||
config,
|
||||
config: pytest.Config,
|
||||
source: Path,
|
||||
dest: Path,
|
||||
hookrecorder,
|
||||
workercontroller,
|
||||
hookrecorder: pytest.HookRecorder,
|
||||
workercontroller: None,
|
||||
) -> None:
|
||||
hm = NodeManager(config, ["popen//chdir=%s" % dest] * 2)
|
||||
hm.roots = []
|
||||
hm.setup_nodes(None)
|
||||
hm.setup_nodes(None) # type: ignore[arg-type]
|
||||
source.joinpath("dir1", "dir2").mkdir(parents=True)
|
||||
source.joinpath("dir1", "dir2", "hello").touch()
|
||||
gw = hm.group[0]
|
||||
@@ -200,7 +210,11 @@ class TestNodeManager:
|
||||
assert p.joinpath("dir1", "file1").check()
|
||||
|
||||
def test_popen_rsync_subdir(
|
||||
self, pytester: pytest.Pytester, source: Path, dest: Path, workercontroller
|
||||
self,
|
||||
pytester: pytest.Pytester,
|
||||
source: Path,
|
||||
dest: Path,
|
||||
workercontroller: None,
|
||||
) -> None:
|
||||
dir1 = source / "dir1"
|
||||
dir1.mkdir()
|
||||
@@ -214,7 +228,8 @@ class TestNodeManager:
|
||||
"--tx", "popen//chdir=%s" % dest, "--rsyncdir", rsyncroot, source
|
||||
)
|
||||
)
|
||||
nodemanager.setup_nodes(None) # calls .rsync_roots()
|
||||
# calls .rsync_roots()
|
||||
nodemanager.setup_nodes(None) # type: ignore[arg-type]
|
||||
if rsyncroot == source:
|
||||
dest = dest.joinpath("source")
|
||||
assert dest.joinpath("dir1").exists()
|
||||
@@ -223,14 +238,19 @@ class TestNodeManager:
|
||||
nodemanager.teardown_nodes()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"flag, expects_report", [("-q", False), ("", False), ("-v", True)]
|
||||
["flag", "expects_report"],
|
||||
[
|
||||
("-q", False),
|
||||
("", False),
|
||||
("-v", True),
|
||||
],
|
||||
)
|
||||
def test_rsync_report(
|
||||
self,
|
||||
pytester: pytest.Pytester,
|
||||
source: Path,
|
||||
dest: Path,
|
||||
workercontroller,
|
||||
workercontroller: None,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
flag: str,
|
||||
expects_report: bool,
|
||||
@@ -241,7 +261,8 @@ class TestNodeManager:
|
||||
if flag:
|
||||
args.append(flag)
|
||||
nodemanager = NodeManager(pytester.parseconfig(*args))
|
||||
nodemanager.setup_nodes(None) # calls .rsync_roots()
|
||||
# calls .rsync_roots()
|
||||
nodemanager.setup_nodes(None) # type: ignore[arg-type]
|
||||
out, _ = capsys.readouterr()
|
||||
if expects_report:
|
||||
assert "<= pytest/__init__.py" in out
|
||||
@@ -249,7 +270,11 @@ class TestNodeManager:
|
||||
assert "<= pytest/__init__.py" not in out
|
||||
|
||||
def test_init_rsync_roots(
|
||||
self, pytester: pytest.Pytester, source: Path, dest: Path, workercontroller
|
||||
self,
|
||||
pytester: pytest.Pytester,
|
||||
source: Path,
|
||||
dest: Path,
|
||||
workercontroller: None,
|
||||
) -> None:
|
||||
dir2 = source.joinpath("dir1", "dir2")
|
||||
dir2.mkdir(parents=True)
|
||||
@@ -267,13 +292,18 @@ class TestNodeManager:
|
||||
)
|
||||
config = pytester.parseconfig(source)
|
||||
nodemanager = NodeManager(config, ["popen//chdir=%s" % dest])
|
||||
nodemanager.setup_nodes(None) # calls .rsync_roots()
|
||||
# calls .rsync_roots()
|
||||
nodemanager.setup_nodes(None) # type: ignore[arg-type]
|
||||
assert dest.joinpath("dir2").exists()
|
||||
assert not dest.joinpath("dir1").exists()
|
||||
assert not dest.joinpath("bogus").exists()
|
||||
|
||||
def test_rsyncignore(
|
||||
self, pytester: pytest.Pytester, source: Path, dest: Path, workercontroller
|
||||
self,
|
||||
pytester: pytest.Pytester,
|
||||
source: Path,
|
||||
dest: Path,
|
||||
workercontroller: None,
|
||||
) -> None:
|
||||
dir2 = source.joinpath("dir1", "dir2")
|
||||
dir2.mkdir(parents=True)
|
||||
@@ -297,7 +327,8 @@ class TestNodeManager:
|
||||
config = pytester.parseconfig(source)
|
||||
config.option.rsyncignore = ["bar"]
|
||||
nodemanager = NodeManager(config, ["popen//chdir=%s" % dest])
|
||||
nodemanager.setup_nodes(None) # calls .rsync_roots()
|
||||
# calls .rsync_roots()
|
||||
nodemanager.setup_nodes(None) # type: ignore[arg-type]
|
||||
assert dest.joinpath("dir1").exists()
|
||||
assert not dest.joinpath("dir1", "dir2").exists()
|
||||
assert dest.joinpath("dir5", "file").exists()
|
||||
@@ -306,14 +337,19 @@ class TestNodeManager:
|
||||
assert not dest.joinpath("bar").exists()
|
||||
|
||||
def test_optimise_popen(
|
||||
self, pytester: pytest.Pytester, source: Path, dest: Path, workercontroller
|
||||
self,
|
||||
pytester: pytest.Pytester,
|
||||
source: Path,
|
||||
dest: Path,
|
||||
workercontroller: None,
|
||||
) -> None:
|
||||
specs = ["popen"] * 3
|
||||
source.joinpath("conftest.py").write_text("rsyncdirs = ['a']")
|
||||
source.joinpath("a").mkdir()
|
||||
config = pytester.parseconfig(source)
|
||||
nodemanager = NodeManager(config, specs)
|
||||
nodemanager.setup_nodes(None) # calls .rysnc_roots()
|
||||
# calls .rysnc_roots()
|
||||
nodemanager.setup_nodes(None) # type: ignore[arg-type]
|
||||
for gwspec in nodemanager.specs:
|
||||
assert gwspec._samefilesystem()
|
||||
assert not gwspec.chdir
|
||||
@@ -349,7 +385,7 @@ class MyWarning(UserWarning):
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_unserialize_warning_msg(w_cls):
|
||||
def test_unserialize_warning_msg(w_cls: type[Warning] | str) -> None:
|
||||
"""Test that warning serialization process works well."""
|
||||
# Create a test warning message
|
||||
with pytest.warns(UserWarning) as w:
|
||||
@@ -390,7 +426,7 @@ class MyWarningUnknown(UserWarning):
|
||||
__module__ = "unknown"
|
||||
|
||||
|
||||
def test_warning_serialization_tweaked_module():
|
||||
def test_warning_serialization_tweaked_module() -> None:
|
||||
"""Test for GH#404."""
|
||||
# Create a test warning message
|
||||
with pytest.warns(UserWarning) as w:
|
||||
|
||||
@@ -5,5 +5,5 @@ class MyWarning2(UserWarning):
|
||||
pass
|
||||
|
||||
|
||||
def generate_warning():
|
||||
def generate_warning() -> None:
|
||||
warnings.warn(MyWarning2("hello"))
|
||||
|
||||
Reference in New Issue
Block a user