Skip to content

Commit 8822644

Browse files
committed
Make xdist opt-in
Tests can fail under xdist in surprising ways. When they fail the failures are hard to debug (and cascade).
1 parent c971607 commit 8822644

3 files changed

Lines changed: 94 additions & 10 deletions

File tree

.github/workflows/test_be.yaml

Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -186,11 +186,21 @@ jobs:
186186
fi
187187
188188
# Test with base dependencies
189-
# Exit code 5 = no tests collected (e.g. only CLI test files changed);
190-
# treat that as success since CLI tests run in a separate workflow.
189+
# Exit code 5 = no tests collected (e.g. only CLI test files changed,
190+
# or no xdist_safe-marked tests in the changed set); treat that as
191+
# success since CLI tests run in a separate workflow and the xdist
192+
# step legitimately collects nothing when no marked modules changed.
191193
#
192-
# TODO: xdist is disabled on Windows because several tests
193-
# crash workers. Fix and re-enable. Failing tests:
194+
# xdist is opt-in: tests run serially by default. Modules opt in by
195+
# adding `pytestmark = pytest.mark.xdist_safe`. On Linux/macOS the
196+
# serial step excludes xdist_safe and a second step runs the marked
197+
# tests under -n auto. On Windows, xdist is disabled entirely and
198+
# the serial step runs every test (marked tests included).
199+
#
200+
# Note: The following tests crashed workers on Windows under xdist.
201+
# If/when any of their modules are opted into xdist_safe, re-verify
202+
# behavior on Windows (Windows will still run them serially, but the
203+
# underlying issues may resurface on Linux/macOS parallel runs):
194204
# - tests/_islands/test_island_generator.py::test_build
195205
# - tests/_islands/test_island_generator.py::test_render
196206
# - tests/_islands/test_island_generator.py::test_render_multiline_markdown
@@ -200,12 +210,29 @@ jobs:
200210
# - tests/_server/test_session_manager.py::test_create_session_absolute_url
201211
# - tests/_server/test_session_manager.py::test_create_session_with_script_config_overrides
202212
# - tests/_server/test_session_manager.py::test_recents_touch_called_on_session_create
203-
- name: Test changed with base dependencies
213+
- name: Test changed with base dependencies (serial)
204214
if: ${{ matrix.dependencies == 'core' }}
205215
run: |
206216
uv run --python ${{ matrix.python-version }} --group test pytest tests/ \
207217
-v \
208-
${{ matrix.os != 'windows-latest' && '-n auto' || '-p no:xdist' }} \
218+
-p no:xdist \
219+
${{ matrix.os != 'windows-latest' && '-m "not xdist_safe"' || '' }} \
220+
-k "not test_cli" \
221+
--durations=10 \
222+
-p packages.pytest_changed \
223+
--changed-from=${{ steps.setup-flags.outputs.changed_from }} \
224+
--include-unchanged=${{ steps.setup-flags.outputs.include_unchanged }} \
225+
--picked=first \
226+
--inline-snapshot=disable \
227+
|| { ec=$?; [ $ec -eq 5 ] && exit 0 || exit $ec; }
228+
229+
- name: Test changed with base dependencies (xdist)
230+
if: ${{ matrix.dependencies == 'core' && matrix.os != 'windows-latest' }}
231+
run: |
232+
uv run --python ${{ matrix.python-version }} --group test pytest tests/ \
233+
-v \
234+
-n auto \
235+
-m xdist_safe \
209236
-k "not test_cli" \
210237
--durations=10 \
211238
-p packages.pytest_changed \
@@ -216,12 +243,29 @@ jobs:
216243
|| { ec=$?; [ $ec -eq 5 ] && exit 0 || exit $ec; }
217244
218245
# Test with optional dependencies
219-
- name: Test changed with optional dependencies
246+
- name: Test changed with optional dependencies (serial)
220247
if: ${{ matrix.dependencies == 'core,optional' }}
221248
run: |
222249
uv run --python ${{ matrix.python-version }} --group test-optional pytest tests/ \
223250
-v \
224-
${{ matrix.os != 'windows-latest' && '-n auto' || '-p no:xdist' }} \
251+
-p no:xdist \
252+
${{ matrix.os != 'windows-latest' && '-m "not xdist_safe"' || '' }} \
253+
-k "not test_cli" \
254+
--durations=10 \
255+
-p packages.pytest_changed \
256+
--changed-from=${{ steps.setup-flags.outputs.changed_from }} \
257+
--include-unchanged=${{ steps.setup-flags.outputs.include_unchanged }} \
258+
--picked=first \
259+
--inline-snapshot=disable \
260+
|| { ec=$?; [ $ec -eq 5 ] && exit 0 || exit $ec; }
261+
262+
- name: Test changed with optional dependencies (xdist)
263+
if: ${{ matrix.dependencies == 'core,optional' && matrix.os != 'windows-latest' }}
264+
run: |
265+
uv run --python ${{ matrix.python-version }} --group test-optional pytest tests/ \
266+
-v \
267+
-n auto \
268+
-m xdist_safe \
225269
-k "not test_cli" \
226270
--durations=10 \
227271
-p packages.pytest_changed \
@@ -234,12 +278,31 @@ jobs:
234278
# Test with minimal dependencies using lowest resolution
235279
# https://docs.astral.sh/uv/concepts/resolution/#lowest-resolution
236280
# https://docs.astral.sh/uv/reference/environment/#uv_resolution
237-
- name: Test with minimal dependencies (lowest resolution)
281+
- name: Test with minimal dependencies (lowest resolution, serial)
238282
if: ${{ matrix.dependencies == 'minimal' }}
239283
run: |
240284
uv run --python ${{ matrix.python-version }} --group test pytest tests/ \
241285
-v \
242-
${{ matrix.os != 'windows-latest' && '-n auto' || '-p no:xdist' }} \
286+
-p no:xdist \
287+
${{ matrix.os != 'windows-latest' && '-m "not xdist_safe"' || '' }} \
288+
-k "not test_cli" \
289+
--durations=10 \
290+
-p packages.pytest_changed \
291+
--changed-from=${{ steps.setup-flags.outputs.changed_from }} \
292+
--include-unchanged=${{ steps.setup-flags.outputs.include_unchanged }} \
293+
--picked=first \
294+
--inline-snapshot=disable \
295+
|| { ec=$?; [ $ec -eq 5 ] && exit 0 || exit $ec; }
296+
env:
297+
UV_RESOLUTION: lowest-direct
298+
299+
- name: Test with minimal dependencies (lowest resolution, xdist)
300+
if: ${{ matrix.dependencies == 'minimal' && matrix.os != 'windows-latest' }}
301+
run: |
302+
uv run --python ${{ matrix.python-version }} --group test pytest tests/ \
303+
-v \
304+
-n auto \
305+
-m xdist_safe \
243306
-k "not test_cli" \
244307
--durations=10 \
245308
-p packages.pytest_changed \

AGENTS.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,26 @@ make fe-check # Typecheck and lint frontend
4141
cd frontend && pnpm test src/path/to/file.test.ts
4242
```
4343

44+
## Parallel tests (xdist)
45+
46+
Backend tests run **serially by default** in CI. Tests are opted into
47+
parallel execution under `pytest-xdist` only after being audited as
48+
independent (no shared global state, no port/file collisions, no reliance
49+
on collection order).
50+
51+
To opt a module in, add near the top of the test file:
52+
53+
```python
54+
import pytest
55+
56+
pytestmark = pytest.mark.xdist_safe
57+
```
58+
59+
Individual tests or classes can also be opted in via
60+
`@pytest.mark.xdist_safe`. If a regression appears under parallel
61+
execution, the fastest fix is to remove the marker from the offending
62+
module and open an issue — do not re-disable xdist globally.
63+
4464
## Commits
4565

4666
- Run `make check` before committing

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,7 @@ markers = [
521521
"unit: marks tests as unit tests",
522522
"flaky: marks tests that are known to be flaky",
523523
"requires(dep1, dep2, ...): requires one or more dependencies to be installed",
524+
"xdist_safe: module/test has been audited as safe to run under pytest-xdist (-n auto)",
524525
]
525526

526527
[tool.coverage.run]

0 commit comments

Comments
 (0)