Skip to content

Commit 096ba6a

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 b8c556b commit 096ba6a

3 files changed

Lines changed: 97 additions & 10 deletions

File tree

.github/actions/test-python/action.yaml

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,21 @@ runs:
5454
fi
5555
5656
# Test with base dependencies
57-
# Exit code 5 = no tests collected (e.g. only CLI test files changed);
58-
# treat that as success since CLI tests run in a separate workflow.
57+
# Exit code 5 = no tests collected (e.g. only CLI test files changed,
58+
# or no xdist_safe-marked tests in the changed set); treat that as
59+
# success since CLI tests run in a separate workflow and the xdist
60+
# step legitimately collects nothing when no marked modules changed.
5961
#
60-
# TODO: xdist is disabled on Windows because several tests
61-
# crash workers. Fix and re-enable. Failing tests:
62+
# xdist is opt-in: tests run serially by default. Modules opt in by
63+
# adding `pytestmark = pytest.mark.xdist_safe`. On Linux/macOS the
64+
# serial step excludes xdist_safe and a second step runs the marked
65+
# tests under -n auto. On Windows, xdist is disabled entirely and
66+
# the serial step runs every test (marked tests included).
67+
#
68+
# Note: The following tests crashed workers on Windows under xdist.
69+
# If/when any of their modules are opted into xdist_safe, re-verify
70+
# behavior on Windows (Windows will still run them serially, but the
71+
# underlying issues may resurface on Linux/macOS parallel runs):
6272
# - tests/_islands/test_island_generator.py::test_build
6373
# - tests/_islands/test_island_generator.py::test_render
6474
# - tests/_islands/test_island_generator.py::test_render_multiline_markdown
@@ -68,13 +78,31 @@ runs:
6878
# - tests/_server/test_session_manager.py::test_create_session_absolute_url
6979
# - tests/_server/test_session_manager.py::test_create_session_with_script_config_overrides
7080
# - tests/_server/test_session_manager.py::test_recents_touch_called_on_session_create
71-
- name: Test changed with base dependencies
81+
- name: Test changed with base dependencies (serial)
7282
if: ${{ inputs.dependencies == 'core' }}
7383
shell: bash
7484
run: |
7585
uv run --python ${{ inputs.python-version }} --group test pytest tests/ \
7686
-v \
77-
${{ inputs.os != 'windows-latest' && '-n auto' || '-p no:xdist' }} \
87+
-p no:xdist \
88+
${{ inputs.os != 'windows-latest' && '-m "not xdist_safe"' || '' }} \
89+
-k "not test_cli" \
90+
--durations=10 \
91+
-p packages.pytest_changed \
92+
--changed-from=${{ steps.setup-flags.outputs.changed_from }} \
93+
--include-unchanged=${{ steps.setup-flags.outputs.include_unchanged }} \
94+
--picked=first \
95+
--inline-snapshot=disable \
96+
|| { ec=$?; [ $ec -eq 5 ] && exit 0 || exit $ec; }
97+
98+
- name: Test changed with base dependencies (xdist)
99+
if: ${{ inputs.dependencies == 'core' && inputs.os != 'windows-latest' }}
100+
shell: bash
101+
run: |
102+
uv run --python ${{ inputs.python-version }} --group test pytest tests/ \
103+
-v \
104+
-n auto \
105+
-m xdist_safe \
78106
-k "not test_cli" \
79107
--durations=10 \
80108
-p packages.pytest_changed \
@@ -85,13 +113,31 @@ runs:
85113
|| { ec=$?; [ $ec -eq 5 ] && exit 0 || exit $ec; }
86114
87115
# Test with optional dependencies
88-
- name: Test changed with optional dependencies
116+
- name: Test changed with optional dependencies (serial)
89117
if: ${{ inputs.dependencies == 'core,optional' }}
90118
shell: bash
91119
run: |
92120
uv run --python ${{ inputs.python-version }} --group test-optional pytest tests/ \
93121
-v \
94-
${{ inputs.os != 'windows-latest' && '-n auto' || '-p no:xdist' }} \
122+
-p no:xdist \
123+
${{ inputs.os != 'windows-latest' && '-m "not xdist_safe"' || '' }} \
124+
-k "not test_cli" \
125+
--durations=10 \
126+
-p packages.pytest_changed \
127+
--changed-from=${{ steps.setup-flags.outputs.changed_from }} \
128+
--include-unchanged=${{ steps.setup-flags.outputs.include_unchanged }} \
129+
--picked=first \
130+
--inline-snapshot=disable \
131+
|| { ec=$?; [ $ec -eq 5 ] && exit 0 || exit $ec; }
132+
133+
- name: Test changed with optional dependencies (xdist)
134+
if: ${{ inputs.dependencies == 'core,optional' && inputs.os != 'windows-latest' }}
135+
shell: bash
136+
run: |
137+
uv run --python ${{ inputs.python-version }} --group test-optional pytest tests/ \
138+
-v \
139+
-n auto \
140+
-m xdist_safe \
95141
-k "not test_cli" \
96142
--durations=10 \
97143
-p packages.pytest_changed \
@@ -104,15 +150,35 @@ runs:
104150
# Test with minimal dependencies using lowest resolution
105151
# https://docs.astral.sh/uv/concepts/resolution/#lowest-resolution
106152
# https://docs.astral.sh/uv/reference/environment/#uv_resolution
107-
- name: Test with minimal dependencies (lowest resolution)
153+
- name: Test with minimal dependencies (lowest resolution, serial)
108154
if: ${{ inputs.dependencies == 'minimal' }}
109155
shell: bash
110156
env:
111157
UV_RESOLUTION: lowest-direct
112158
run: |
113159
uv run --python ${{ inputs.python-version }} --group test pytest tests/ \
114160
-v \
115-
${{ inputs.os != 'windows-latest' && '-n auto' || '-p no:xdist' }} \
161+
-p no:xdist \
162+
${{ inputs.os != 'windows-latest' && '-m "not xdist_safe"' || '' }} \
163+
-k "not test_cli" \
164+
--durations=10 \
165+
-p packages.pytest_changed \
166+
--changed-from=${{ steps.setup-flags.outputs.changed_from }} \
167+
--include-unchanged=${{ steps.setup-flags.outputs.include_unchanged }} \
168+
--picked=first \
169+
--inline-snapshot=disable \
170+
|| { ec=$?; [ $ec -eq 5 ] && exit 0 || exit $ec; }
171+
172+
- name: Test with minimal dependencies (lowest resolution, xdist)
173+
if: ${{ inputs.dependencies == 'minimal' && inputs.os != 'windows-latest' }}
174+
shell: bash
175+
env:
176+
UV_RESOLUTION: lowest-direct
177+
run: |
178+
uv run --python ${{ inputs.python-version }} --group test pytest tests/ \
179+
-v \
180+
-n auto \
181+
-m xdist_safe \
116182
-k "not test_cli" \
117183
--durations=10 \
118184
-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)