Skip to content

Commit e5f3663

Browse files
authored
Speed up test suite with pytest-xdist (#2537)
* Speed up test suite with pytest-xdist * add combine * Check if loadgroup solves windows issues * Add xdist group to multiprocess tests * Skip reload tests on windows and mac * skip non linux * skip non linux * add not linux * add last not linux * skip another * skip another * Update tests/supervisors/test_reload.py
1 parent 2445e79 commit e5f3663

File tree

9 files changed

+37
-23
lines changed

9 files changed

+37
-23
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.cache
22
.coverage
3+
.coverage.*
34
.mypy_cache/
45
__pycache__/
56
uvicorn.egg-info/

pyproject.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ disallow_untyped_defs = false
8585
check_untyped_defs = true
8686

8787
[tool.pytest.ini_options]
88-
addopts = "-rxXs --strict-config --strict-markers"
88+
addopts = "-rxXs --strict-config --strict-markers -n 8"
8989
xfail_strict = true
9090
filterwarnings = [
9191
"error",
@@ -95,6 +95,7 @@ filterwarnings = [
9595
]
9696

9797
[tool.coverage.run]
98+
parallel = true
9899
source_pkgs = ["uvicorn", "tests"]
99100
plugins = ["coverage_conditional_plugin"]
100101
omit = ["uvicorn/workers.py", "uvicorn/__main__.py"]
@@ -125,6 +126,7 @@ exclude_lines = [
125126
py-win32 = "sys_platform == 'win32'"
126127
py-not-win32 = "sys_platform != 'win32'"
127128
py-linux = "sys_platform == 'linux'"
129+
py-not-linux = "sys_platform != 'linux'"
128130
py-darwin = "sys_platform == 'darwin'"
129131
py-gte-39 = "sys_version_info >= (3, 9)"
130132
py-lt-39 = "sys_version_info < (3, 9)"

requirements.txt

+2
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ twine==6.1.0
1717
ruff==0.9.9
1818
pytest==8.3.4
1919
pytest-mock==3.14.0
20+
pytest-xdist[psutil]==3.6.0
2021
mypy==1.15.0
2122
types-click==7.1.8
2223
types-pyyaml==6.0.12.20241230
2324
trustme==1.2.1
2425
cryptography==44.0.1
2526
coverage==7.6.12
2627
coverage-conditional-plugin==0.9.0
28+
coverage-enable-subprocess==1.0
2729
httpx==0.28.1
2830

2931
# Documentation

scripts/coverage

+1
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ export SOURCE_FILES="uvicorn tests"
88

99
set -x
1010

11+
${PREFIX}coverage combine
1112
${PREFIX}coverage report

scripts/test

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ if [ -z $GITHUB_ACTIONS ]; then
1111
scripts/check
1212
fi
1313

14+
export COVERAGE_PROCESS_START=$(pwd)/pyproject.toml
15+
1416
${PREFIX}coverage run --debug config -m pytest "$@"
1517

1618
if [ -z $GITHUB_ACTIONS ]; then

tests/supervisors/test_reload.py

+22-18
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
import platform
43
import signal
54
import socket
65
import sys
@@ -24,11 +23,8 @@
2423
WatchFilesReload = None # type: ignore[misc,assignment]
2524

2625

27-
# TODO: Investigate why this is flaky on MacOS M1.
28-
skip_if_m1 = pytest.mark.skipif(
29-
sys.platform == "darwin" and platform.processor() == "arm",
30-
reason="Flaky on MacOS M1",
31-
)
26+
# TODO: Investigate why this is flaky on MacOS, and Windows.
27+
skip_non_linux = pytest.mark.skipif(sys.platform in ("darwin", "win32"), reason="Flaky on Windows and MacOS")
3228

3329

3430
def run(sockets: list[socket.socket] | None) -> None:
@@ -141,8 +137,12 @@ def test_should_not_reload_when_python_file_in_excluded_subdir_is_changed(self,
141137

142138
reloader.shutdown()
143139

144-
@pytest.mark.parametrize("reloader_class, result", [(StatReload, False), (WatchFilesReload, True)])
145-
def test_reload_when_pattern_matched_file_is_changed(self, result: bool, touch_soon: Callable[[Path], None]):
140+
@pytest.mark.parametrize(
141+
"reloader_class, result", [(StatReload, False), pytest.param(WatchFilesReload, True, marks=skip_non_linux)]
142+
)
143+
def test_reload_when_pattern_matched_file_is_changed(
144+
self, result: bool, touch_soon: Callable[[Path], None]
145+
): # pragma: py-not-linux
146146
file = self.reload_path / "app" / "js" / "main.js"
147147

148148
with as_cwd(self.reload_path):
@@ -153,10 +153,10 @@ def test_reload_when_pattern_matched_file_is_changed(self, result: bool, touch_s
153153

154154
reloader.shutdown()
155155

156-
@pytest.mark.parametrize("reloader_class", [pytest.param(WatchFilesReload, marks=skip_if_m1)])
156+
@pytest.mark.parametrize("reloader_class", [pytest.param(WatchFilesReload, marks=skip_non_linux)])
157157
def test_should_not_reload_when_exclude_pattern_match_file_is_changed(
158158
self, touch_soon: Callable[[Path], None]
159-
): # pragma: py-darwin
159+
): # pragma: py-not-linux
160160
python_file = self.reload_path / "app" / "src" / "main.py"
161161
css_file = self.reload_path / "app" / "css" / "main.css"
162162
js_file = self.reload_path / "app" / "js" / "main.js"
@@ -188,8 +188,10 @@ def test_should_not_reload_when_dot_file_is_changed(self, touch_soon: Callable[[
188188

189189
reloader.shutdown()
190190

191-
@pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
192-
def test_should_reload_when_directories_have_same_prefix(self, touch_soon: Callable[[Path], None]):
191+
@pytest.mark.parametrize("reloader_class", [StatReload, pytest.param(WatchFilesReload, marks=skip_non_linux)])
192+
def test_should_reload_when_directories_have_same_prefix(
193+
self, touch_soon: Callable[[Path], None]
194+
): # pragma: py-not-linux
193195
app_dir = self.reload_path / "app"
194196
app_file = app_dir / "src" / "main.py"
195197
app_first_dir = self.reload_path / "app_first"
@@ -210,9 +212,11 @@ def test_should_reload_when_directories_have_same_prefix(self, touch_soon: Calla
210212

211213
@pytest.mark.parametrize(
212214
"reloader_class",
213-
[StatReload, pytest.param(WatchFilesReload, marks=skip_if_m1)],
215+
[StatReload, pytest.param(WatchFilesReload, marks=skip_non_linux)],
214216
)
215-
def test_should_not_reload_when_only_subdirectory_is_watched(self, touch_soon: Callable[[Path], None]):
217+
def test_should_not_reload_when_only_subdirectory_is_watched(
218+
self, touch_soon: Callable[[Path], None]
219+
): # pragma: py-not-linux
216220
app_dir = self.reload_path / "app"
217221
app_dir_file = self.reload_path / "app" / "src" / "main.py"
218222
root_file = self.reload_path / "main.py"
@@ -229,8 +233,8 @@ def test_should_not_reload_when_only_subdirectory_is_watched(self, touch_soon: C
229233

230234
reloader.shutdown()
231235

232-
@pytest.mark.parametrize("reloader_class", [pytest.param(WatchFilesReload, marks=skip_if_m1)])
233-
def test_override_defaults(self, touch_soon: Callable[[Path], None]) -> None: # pragma: py-darwin
236+
@pytest.mark.parametrize("reloader_class", [pytest.param(WatchFilesReload, marks=skip_non_linux)])
237+
def test_override_defaults(self, touch_soon: Callable[[Path], None]) -> None: # pragma: py-not-linux
234238
dotted_file = self.reload_path / ".dotted"
235239
dotted_dir_file = self.reload_path / ".dotted_dir" / "file.txt"
236240
python_file = self.reload_path / "main.py"
@@ -251,8 +255,8 @@ def test_override_defaults(self, touch_soon: Callable[[Path], None]) -> None: #
251255

252256
reloader.shutdown()
253257

254-
@pytest.mark.parametrize("reloader_class", [pytest.param(WatchFilesReload, marks=skip_if_m1)])
255-
def test_explicit_paths(self, touch_soon: Callable[[Path], None]) -> None: # pragma: py-darwin
258+
@pytest.mark.parametrize("reloader_class", [pytest.param(WatchFilesReload, marks=skip_non_linux)])
259+
def test_explicit_paths(self, touch_soon: Callable[[Path], None]) -> None: # pragma: py-not-linux
256260
dotted_file = self.reload_path / ".dotted"
257261
non_dotted_file = self.reload_path / "ext" / "ext.jpg"
258262
python_file = self.reload_path / "main.py"

tests/test_server.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable
6464
@pytest.mark.parametrize("exception_signal", signals)
6565
@pytest.mark.parametrize("capture_signal", signal_captures)
6666
async def test_server_interrupt(
67-
exception_signal: signal.Signals, capture_signal: Callable[[signal.Signals], AbstractContextManager[None]]
67+
exception_signal: signal.Signals,
68+
capture_signal: Callable[[signal.Signals], AbstractContextManager[None]],
69+
unused_tcp_port: int,
6870
): # pragma: py-win32
6971
"""Test interrupting a Server that is run explicitly inside asyncio"""
7072

@@ -73,7 +75,7 @@ async def interrupt_running(srv: Server):
7375
await asyncio.sleep(0.01)
7476
signal.raise_signal(exception_signal)
7577

76-
server = Server(Config(app=dummy_app, loop="asyncio"))
78+
server = Server(Config(app=dummy_app, loop="asyncio", port=unused_tcp_port))
7779
asyncio.create_task(interrupt_running(server))
7880
with capture_signal(exception_signal) as witness:
7981
await server.serve()

uvicorn/config.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def resolve_reload_patterns(patterns_list: list[str], directories_list: list[str
138138
# Special case for the .* pattern, otherwise this would only match
139139
# hidden directories which is probably undesired
140140
if pattern == ".*":
141-
continue # pragma: py-darwin
141+
continue # pragma: py-not-linux
142142
patterns.append(pattern)
143143
if is_dir(Path(pattern)):
144144
directories.append(Path(pattern))

uvicorn/server.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def create_protocol(
119119

120120
def _share_socket(
121121
sock: socket.SocketType,
122-
) -> socket.SocketType: # pragma py-linux pragma: py-darwin
122+
) -> socket.SocketType: # pragma py-not-win32
123123
# Windows requires the socket be explicitly shared across
124124
# multiple workers (processes).
125125
from socket import fromshare # type: ignore[attr-defined]

0 commit comments

Comments
 (0)