Improve typing

Fix #1057
This commit is contained in:
Ran Benita
2023-04-18 23:06:34 +03:00
parent 5dfc590a8c
commit b78cf1c0ce
24 changed files with 858 additions and 477 deletions

View File

@@ -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(
"""

View File

@@ -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:

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -5,5 +5,5 @@ class MyWarning2(UserWarning):
pass
def generate_warning():
def generate_warning() -> None:
warnings.warn(MyWarning2("hello"))