Skip to content

Commit 987c2ca

Browse files
Merge pull request #1171 from Alon-L/feature/proxy-gateways
Add proxy gateways
2 parents 59f7434 + 8346acc commit 987c2ca

File tree

10 files changed

+83
-14
lines changed

10 files changed

+83
-14
lines changed

changelog/1170.feature

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add the `--px` arg to create proxy gateways.
2+
3+
Proxy gateways are passed to additional gateways using the `via` keyword.
4+
They can serve as a way to run multiple workers on remote machines.

docs/remote.rst

+14
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,20 @@ new socket host with something like this::
6666
pytest -d --tx socket=192.168.1.102:8888 --rsyncdir mypkg
6767

6868

69+
Using proxies to run multiple workers on remote machines
70+
---------------------------------------
71+
72+
In case you want to run multiple workers on a remote machine,
73+
you can create a proxy gateway for the machine, and run multiple
74+
workers using the `via` attribute.::
75+
76+
pytest -d --px id=my_proxy//socket=192.168.1.102:8888 --tx 5*popen//via=my_proxy
77+
78+
Here we declare a proxy gateway using the `--px` arg, and
79+
create 5 workers that run on the remote server using the proxy.
80+
Note that the proxy gateway does not run a worker, thus only 5
81+
workers are created.
82+
6983

7084
Running tests on many platforms at once
7185
---------------------------------------

src/xdist/plugin.py

+11
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,17 @@ def pytest_addoption(parser: pytest.Parser) -> None:
139139
"--tx ssh=user@codespeak.net//chdir=testcache"
140140
),
141141
)
142+
group.addoption(
143+
"--px",
144+
dest="px",
145+
action="append",
146+
default=[],
147+
metavar="xspec",
148+
help=(
149+
"Add a proxy gateway to pass to test execution environments using `via`. Example:\n"
150+
"--px id=my_proxy//socket=192.168.1.102:8888 --tx 5*popen//via=my_proxy"
151+
),
152+
)
142153
group._addoption(
143154
"-d",
144155
action="store_true",

src/xdist/scheduler/each.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from xdist.remote import Producer
88
from xdist.report import report_collection_diff
9-
from xdist.workermanage import parse_spec_config
9+
from xdist.workermanage import parse_tx_spec_config
1010
from xdist.workermanage import WorkerController
1111

1212

@@ -26,7 +26,7 @@ class EachScheduling:
2626

2727
def __init__(self, config: pytest.Config, log: Producer | None = None) -> None:
2828
self.config = config
29-
self.numnodes = len(parse_spec_config(config))
29+
self.numnodes = len(parse_tx_spec_config(config))
3030
self.node2collection: dict[WorkerController, list[str]] = {}
3131
self.node2pending: dict[WorkerController, list[int]] = {}
3232
self._started: list[WorkerController] = []

src/xdist/scheduler/load.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from xdist.remote import Producer
99
from xdist.report import report_collection_diff
10-
from xdist.workermanage import parse_spec_config
10+
from xdist.workermanage import parse_tx_spec_config
1111
from xdist.workermanage import WorkerController
1212

1313

@@ -58,7 +58,7 @@ class LoadScheduling:
5858
"""
5959

6060
def __init__(self, config: pytest.Config, log: Producer | None = None) -> None:
61-
self.numnodes = len(parse_spec_config(config))
61+
self.numnodes = len(parse_tx_spec_config(config))
6262
self.node2collection: dict[WorkerController, list[str]] = {}
6363
self.node2pending: dict[WorkerController, list[int]] = {}
6464
self.pending: list[int] = []

src/xdist/scheduler/loadscope.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from xdist.remote import Producer
1010
from xdist.report import report_collection_diff
11-
from xdist.workermanage import parse_spec_config
11+
from xdist.workermanage import parse_tx_spec_config
1212
from xdist.workermanage import WorkerController
1313

1414

@@ -91,7 +91,7 @@ class LoadScopeScheduling:
9191
"""
9292

9393
def __init__(self, config: pytest.Config, log: Producer | None = None) -> None:
94-
self.numnodes = len(parse_spec_config(config))
94+
self.numnodes = len(parse_tx_spec_config(config))
9595
self.collection: list[str] | None = None
9696

9797
self.workqueue: OrderedDict[str, dict[str, bool]] = OrderedDict()

src/xdist/scheduler/worksteal.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from xdist.remote import Producer
99
from xdist.report import report_collection_diff
10-
from xdist.workermanage import parse_spec_config
10+
from xdist.workermanage import parse_tx_spec_config
1111
from xdist.workermanage import WorkerController
1212

1313

@@ -65,7 +65,7 @@ class WorkStealingScheduling:
6565
"""
6666

6767
def __init__(self, config: pytest.Config, log: Producer | None = None) -> None:
68-
self.numnodes = len(parse_spec_config(config))
68+
self.numnodes = len(parse_tx_spec_config(config))
6969
self.node2collection: dict[WorkerController, list[str]] = {}
7070
self.node2pending: dict[WorkerController, list[int]] = {}
7171
self.pending: list[int] = []

src/xdist/workermanage.py

+16-4
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from xdist.remote import WorkerInfo
2424

2525

26-
def parse_spec_config(config: pytest.Config) -> list[str]:
26+
def parse_tx_spec_config(config: pytest.Config) -> list[str]:
2727
xspeclist = []
2828
tx: list[str] = config.getvalue("tx")
2929
for xspec in tx:
@@ -57,8 +57,17 @@ def __init__(
5757
if self.testrunuid is None:
5858
self.testrunuid = uuid.uuid4().hex
5959
self.group = execnet.Group(execmodel="main_thread_only")
60+
for proxy_spec in self._getpxspecs():
61+
# Proxy gateways do not run workers, and are meant to be passed with the `via` attribute
62+
# to additional gateways.
63+
# They are useful for running multiple workers on remote machines.
64+
if getattr(proxy_spec, "id", None) is None:
65+
raise pytest.UsageError(
66+
f"Proxy gateway {proxy_spec} must include an id"
67+
)
68+
self.group.makegateway(proxy_spec)
6069
if specs is None:
61-
specs = self._getxspecs()
70+
specs = self._gettxspecs()
6271
self.specs: list[execnet.XSpec] = []
6372
for spec in specs:
6473
if not isinstance(spec, execnet.XSpec):
@@ -107,8 +116,11 @@ def setup_node(
107116
def teardown_nodes(self) -> None:
108117
self.group.terminate(self.EXIT_TIMEOUT)
109118

110-
def _getxspecs(self) -> list[execnet.XSpec]:
111-
return [execnet.XSpec(x) for x in parse_spec_config(self.config)]
119+
def _gettxspecs(self) -> list[execnet.XSpec]:
120+
return [execnet.XSpec(x) for x in parse_tx_spec_config(self.config)]
121+
122+
def _getpxspecs(self) -> list[execnet.XSpec]:
123+
return [execnet.XSpec(x) for x in self.config.getoption("px")]
112124

113125
def _getrsyncdirs(self) -> list[Path]:
114126
for spec in self.specs:

testing/test_plugin.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -295,15 +295,15 @@ class TestDistOptions:
295295
def test_getxspecs(self, pytester: pytest.Pytester) -> None:
296296
config = pytester.parseconfigure("--tx=popen", "--tx", "ssh=xyz")
297297
nodemanager = NodeManager(config)
298-
xspecs = nodemanager._getxspecs()
298+
xspecs = nodemanager._gettxspecs()
299299
assert len(xspecs) == 2
300300
print(xspecs)
301301
assert xspecs[0].popen
302302
assert xspecs[1].ssh == "xyz"
303303

304304
def test_xspecs_multiplied(self, pytester: pytest.Pytester) -> None:
305305
config = pytester.parseconfigure("--tx=3*popen")
306-
xspecs = NodeManager(config)._getxspecs()
306+
xspecs = NodeManager(config)._gettxspecs()
307307
assert len(xspecs) == 3
308308
assert xspecs[1].popen
309309

testing/test_workermanage.py

+28
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,34 @@ def test_one():
368368
(rep,) = reprec.getreports("pytest_runtest_logreport")
369369
assert rep.passed
370370

371+
def test_proxy_gateway_setup_nodes(self, pytester: pytest.Pytester) -> None:
372+
nodemanager = NodeManager(
373+
pytester.parseconfig(
374+
"--px", "popen//id=my_proxy", "--tx", "popen//via=my_proxy"
375+
)
376+
)
377+
nodes = nodemanager.setup_nodes(None) # type: ignore[arg-type]
378+
379+
# Proxy gateways are considered as an execnet gateway
380+
assert len(nodemanager.group) == 2
381+
382+
# Proxy gateways do not run workers
383+
assert len(nodes) == 1
384+
385+
def test_proxy_gateway(self, pytester: pytest.Pytester) -> None:
386+
pytester.makepyfile(
387+
__init__="",
388+
test_x="""
389+
def test_one():
390+
pass
391+
""",
392+
)
393+
reprec = pytester.inline_run(
394+
"-d", "--px", "popen//id=my_proxy", "--tx", "popen//via=my_proxy"
395+
)
396+
rep = reprec.getreports("pytest_runtest_logreport")
397+
assert rep[1].passed
398+
371399

372400
class MyWarning(UserWarning):
373401
pass

0 commit comments

Comments
 (0)