Skip to content

Commit cdc2ee5

Browse files
authored
feat(e2e): parallelize e2e tests with pytest-xdist (default -n 5) (#102)
Closes #101 Add pytest-xdist for parallel e2e test execution with configurable concurrency. Default to 5 workers; override via E2E_PARALLEL env var (accepts a number or 'auto' for CPU-count matching). Make session-scoped mock inference route fixtures worker-safe by incorporating the xdist worker_id into route names and routing hints. Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
1 parent dceb539 commit cdc2ee5

5 files changed

Lines changed: 62 additions & 16 deletions

File tree

e2e/python/conftest.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,25 @@ def inference_client(sandbox_client: SandboxClient) -> InferenceRouteClient:
7272
return InferenceRouteClient.from_sandbox_client(sandbox_client)
7373

7474

75+
@pytest.fixture(scope="session")
76+
def _worker_suffix(worker_id: str) -> str:
77+
"""Return a suffix for worker-unique resource names.
78+
79+
Uses the built-in ``worker_id`` fixture from pytest-xdist which returns
80+
``"gw0"``, ``"gw1"``, etc. for workers, or ``"master"`` for non-xdist runs.
81+
"""
82+
if worker_id == "master":
83+
return ""
84+
return f"-{worker_id}"
85+
86+
7587
@pytest.fixture(scope="session")
7688
def mock_inference_route(
7789
inference_client: InferenceRouteClient,
90+
_worker_suffix: str,
7891
) -> Iterator[str]:
79-
name = "e2e-mock-local"
80-
routing_hint = "e2e_mock_local"
92+
name = f"e2e-mock-local{_worker_suffix}"
93+
routing_hint = f"e2e_mock_local{_worker_suffix}"
8194
# Clean up any leftover route from a previous run.
8295
try:
8396
inference_client.delete(name)
@@ -93,7 +106,7 @@ def mock_inference_route(
93106
model_id="mock/test-model",
94107
enabled=True,
95108
)
96-
yield name
109+
yield routing_hint
97110
try:
98111
inference_client.delete(name)
99112
except grpc.RpcError:
@@ -103,9 +116,10 @@ def mock_inference_route(
103116
@pytest.fixture(scope="session")
104117
def mock_anthropic_route(
105118
inference_client: InferenceRouteClient,
119+
_worker_suffix: str,
106120
) -> Iterator[str]:
107-
name = "e2e-mock-anthropic"
108-
routing_hint = "e2e_mock_anthropic"
121+
name = f"e2e-mock-anthropic{_worker_suffix}"
122+
routing_hint = f"e2e_mock_anthropic{_worker_suffix}"
109123
try:
110124
inference_client.delete(name)
111125
except grpc.RpcError:
@@ -120,7 +134,7 @@ def mock_anthropic_route(
120134
model_id="mock/claude-test",
121135
enabled=True,
122136
)
123-
yield name
137+
yield routing_hint
124138
try:
125139
inference_client.delete(name)
126140
except grpc.RpcError:
@@ -130,10 +144,11 @@ def mock_anthropic_route(
130144
@pytest.fixture(scope="session")
131145
def mock_disallowed_route(
132146
inference_client: InferenceRouteClient,
147+
_worker_suffix: str,
133148
) -> Iterator[str]:
134149
"""Route that exists but is NOT in any sandbox's allowed_routes."""
135-
name = "e2e-mock-disallowed"
136-
routing_hint = "e2e_mock_disallowed"
150+
name = f"e2e-mock-disallowed{_worker_suffix}"
151+
routing_hint = f"e2e_mock_disallowed{_worker_suffix}"
137152
try:
138153
inference_client.delete(name)
139154
except grpc.RpcError:
@@ -148,7 +163,7 @@ def mock_disallowed_route(
148163
model_id="mock/disallowed-model",
149164
enabled=True,
150165
)
151-
yield name
166+
yield routing_hint
152167
try:
153168
inference_client.delete(name)
154169
except grpc.RpcError:

e2e/python/test_inference_routing.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,9 @@ def test_inference_call_routed_to_backend(
177177
4. Forward locally via sandbox router to the policy-allowed backend
178178
5. Return the mock response from the configured route
179179
"""
180-
spec = datamodel_pb2.SandboxSpec(policy=_inference_routing_policy())
180+
spec = datamodel_pb2.SandboxSpec(
181+
policy=_inference_routing_policy(mock_inference_route)
182+
)
181183

182184
def call_chat_completions() -> str:
183185
import json
@@ -226,7 +228,9 @@ def test_non_inference_request_denied(
226228
undeclared endpoint should be denied with 403 when inference routing
227229
is configured — only recognized inference API patterns are routed.
228230
"""
229-
spec = datamodel_pb2.SandboxSpec(policy=_inference_routing_policy())
231+
spec = datamodel_pb2.SandboxSpec(
232+
policy=_inference_routing_policy(mock_inference_route)
233+
)
230234

231235
def make_non_inference_request() -> str:
232236
import ssl
@@ -265,7 +269,7 @@ def test_inference_anthropic_messages_protocol(
265269
policy = sandbox_pb2.SandboxPolicy(
266270
version=1,
267271
inference=sandbox_pb2.InferencePolicy(
268-
allowed_routes=["e2e_mock_anthropic"],
272+
allowed_routes=[mock_anthropic_route],
269273
),
270274
filesystem=_BASE_FILESYSTEM,
271275
landlock=_BASE_LANDLOCK,
@@ -323,8 +327,10 @@ def test_inference_route_filtering_by_allowed_routes(
323327
allowed route should succeed, while inference requests that can't
324328
match any allowed route get an error from the sandbox router.
325329
"""
326-
# Policy only allows e2e_mock_local, NOT e2e_mock_disallowed
327-
spec = datamodel_pb2.SandboxSpec(policy=_inference_routing_policy())
330+
# Policy only allows the mock_inference_route, NOT mock_disallowed_route
331+
spec = datamodel_pb2.SandboxSpec(
332+
policy=_inference_routing_policy(mock_inference_route)
333+
)
328334

329335
def call_allowed_route() -> str:
330336
import json

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ dev = [
3131
"pytest>=8.0",
3232
"pytest-asyncio>=0.23",
3333
"pytest-cov>=4.0",
34+
"pytest-xdist>=3.0",
3435
"ruff>=0.4",
3536
"ty>=0.0.1a6",
3637
"maturin>=1.5,<2.0",

tasks/test.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ run = "uv run pytest python/"
2929
hide = true
3030

3131
["test:e2e:sandbox"]
32-
description = "Run sandbox end-to-end tests"
32+
description = "Run sandbox end-to-end tests (E2E_PARALLEL=N or 'auto'; default 5)"
3333
depends = ["python:proto", "cluster"]
3434
env = { UV_NO_SYNC = "1", PYTHONPATH = "python" }
35-
run = "uv run pytest -o python_files='test_*.py' e2e/python"
35+
run = "uv run pytest -o python_files='test_*.py' -n ${E2E_PARALLEL:-5} e2e/python"
3636
hide = true
3737

3838
["test:e2e:port-forward"]

uv.lock

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)