1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
|
From 82592c9815409a1e023152f565a65b0105565ed2 Mon Sep 17 00:00:00 2001
From: Mariusz Felisiak <felisiak.mariusz@gmail.com>
Date: Mon, 28 Jul 2025 23:21:17 +0200
Subject: [PATCH] Fixed #36531 -- Added forkserver support to parallel test
runner.
---
django/db/backends/sqlite3/creation.py | 10 +++++-----
django/test/runner.py | 16 ++++++++++------
tests/backends/sqlite/test_creation.py | 4 ++--
tests/test_runner/test_discover_runner.py | 10 ++++++++++
4 files changed, 27 insertions(+), 13 deletions(-)
diff --git a/django/db/backends/sqlite3/creation.py b/django/db/backends/sqlite3/creation.py
index 802e8b8357..8a07e0c417 100644
--- a/django/db/backends/sqlite3/creation.py
+++ b/django/db/backends/sqlite3/creation.py
@@ -62,7 +62,7 @@ class DatabaseCreation(BaseDatabaseCreation):
start_method = multiprocessing.get_start_method()
if start_method == "fork":
return orig_settings_dict
- if start_method == "spawn":
+ if start_method in {"forkserver", "spawn"}:
return {
**orig_settings_dict,
"NAME": f"{self.connection.alias}_{suffix}.sqlite3",
@@ -99,9 +99,9 @@ class DatabaseCreation(BaseDatabaseCreation):
self.log("Got an error cloning the test database: %s" % e)
sys.exit(2)
# Forking automatically makes a copy of an in-memory database.
- # Spawn requires migrating to disk which will be re-opened in
- # setup_worker_connection.
- elif multiprocessing.get_start_method() == "spawn":
+ # Forkserver and spawn require migrating to disk which will be
+ # re-opened in setup_worker_connection.
+ elif multiprocessing.get_start_method() in {"forkserver", "spawn"}:
ondisk_db = sqlite3.connect(target_database_name, uri=True)
self.connection.connection.backup(ondisk_db)
ondisk_db.close()
@@ -137,7 +137,7 @@ class DatabaseCreation(BaseDatabaseCreation):
# Update settings_dict in place.
self.connection.settings_dict.update(settings_dict)
self.connection.close()
- elif start_method == "spawn":
+ elif start_method in {"forkserver", "spawn"}:
alias = self.connection.alias
connection_str = (
f"file:memorydb_{alias}_{_worker_id}?mode=memory&cache=shared"
diff --git a/django/test/runner.py b/django/test/runner.py
index b83cd37343..cc2fb2ebdf 100644
--- a/django/test/runner.py
+++ b/django/test/runner.py
@@ -387,8 +387,9 @@ def get_max_test_processes():
The maximum number of test processes when using the --parallel option.
"""
# The current implementation of the parallel test runner requires
- # multiprocessing to start subprocesses with fork() or spawn().
- if multiprocessing.get_start_method() not in {"fork", "spawn"}:
+ # multiprocessing to start subprocesses with fork(), forkserver(), or
+ # spawn().
+ if multiprocessing.get_start_method() not in {"fork", "spawn", "forkserver"}:
return 1
try:
return int(os.environ["DJANGO_TEST_PROCESSES"])
@@ -433,9 +434,12 @@ def _init_worker(
counter.value += 1
_worker_id = counter.value
- start_method = multiprocessing.get_start_method()
+ is_spawn_or_forkserver = multiprocessing.get_start_method() in {
+ "forkserver",
+ "spawn",
+ }
- if start_method == "spawn":
+ if is_spawn_or_forkserver:
if process_setup and callable(process_setup):
if process_setup_args is None:
process_setup_args = ()
@@ -446,7 +450,7 @@ def _init_worker(
db_aliases = used_aliases if used_aliases is not None else connections
for alias in db_aliases:
connection = connections[alias]
- if start_method == "spawn":
+ if is_spawn_or_forkserver:
# Restore initial settings in spawned processes.
connection.settings_dict.update(initial_settings[alias])
if value := serialized_contents.get(alias):
@@ -589,7 +593,7 @@ class ParallelTestSuite(unittest.TestSuite):
return iter(self.subsuites)
def initialize_suite(self):
- if multiprocessing.get_start_method() == "spawn":
+ if multiprocessing.get_start_method() in {"forkserver", "spawn"}:
self.initial_settings = {
alias: connections[alias].settings_dict for alias in connections
}
diff --git a/tests/backends/sqlite/test_creation.py b/tests/backends/sqlite/test_creation.py
index 8aa24674d2..fe3959c85b 100644
--- a/tests/backends/sqlite/test_creation.py
+++ b/tests/backends/sqlite/test_creation.py
@@ -36,8 +36,8 @@ class TestDbSignatureTests(SimpleTestCase):
clone_settings_dict = creation_class.get_test_db_clone_settings("1")
self.assertEqual(clone_settings_dict["NAME"], expected_clone_name)
- @mock.patch.object(multiprocessing, "get_start_method", return_value="forkserver")
+ @mock.patch.object(multiprocessing, "get_start_method", return_value="unsupported")
def test_get_test_db_clone_settings_not_supported(self, *mocked_objects):
- msg = "Cloning with start method 'forkserver' is not supported."
+ msg = "Cloning with start method 'unsupported' is not supported."
with self.assertRaisesMessage(NotSupportedError, msg):
connection.creation.get_test_db_clone_settings(1)
diff --git a/tests/test_runner/test_discover_runner.py b/tests/test_runner/test_discover_runner.py
index 4f13cceeff..c6ce7f1e1d 100644
--- a/tests/test_runner/test_discover_runner.py
+++ b/tests/test_runner/test_discover_runner.py
@@ -98,6 +98,16 @@ class DiscoverRunnerParallelArgumentTests(SimpleTestCase):
mocked_cpu_count,
):
mocked_get_start_method.return_value = "forkserver"
+ self.assertEqual(get_max_test_processes(), 12)
+ with mock.patch.dict(os.environ, {"DJANGO_TEST_PROCESSES": "7"}):
+ self.assertEqual(get_max_test_processes(), 7)
+
+ def test_get_max_test_processes_other(
+ self,
+ mocked_get_start_method,
+ mocked_cpu_count,
+ ):
+ mocked_get_start_method.return_value = "other"
self.assertEqual(get_max_test_processes(), 1)
with mock.patch.dict(os.environ, {"DJANGO_TEST_PROCESSES": "7"}):
self.assertEqual(get_max_test_processes(), 1)
|