Skip to content

Commit 7f19186

Browse files
authored
PYTHON-3406 Refactor fork tests to print traceback on failure (#1042)
1 parent a0a5c71 commit 7f19186

File tree

3 files changed

+65
-90
lines changed

3 files changed

+65
-90
lines changed

test/__init__.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
from contextlib import contextmanager
4444
from functools import wraps
4545
from test.version import Version
46-
from typing import Dict, no_type_check
46+
from typing import Dict, Generator, no_type_check
4747
from unittest import SkipTest
4848
from urllib.parse import quote_plus
4949

@@ -998,6 +998,33 @@ def fail_point(self, command_args):
998998
"configureFailPoint", cmd_on["configureFailPoint"], mode="off"
999999
)
10001000

1001+
@contextmanager
1002+
def fork(self) -> Generator[int, None, None]:
1003+
"""Helper for tests that use os.fork()
1004+
1005+
Use in a with statement:
1006+
1007+
with self.fork() as pid:
1008+
if pid == 0: # Child
1009+
pass
1010+
else: # Parent
1011+
pass
1012+
"""
1013+
pid = os.fork()
1014+
in_child = pid == 0
1015+
try:
1016+
yield pid
1017+
except:
1018+
if in_child:
1019+
traceback.print_exc()
1020+
os._exit(1)
1021+
raise
1022+
finally:
1023+
if in_child:
1024+
os._exit(0)
1025+
# In parent, assert child succeeded.
1026+
self.assertEqual(0, os.waitpid(pid, 0)[1])
1027+
10011028

10021029
class IntegrationTest(PyMongoTestCase):
10031030
"""Base class for TestCases that need a connection to MongoDB to pass."""

test/test_encryption.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,6 @@ def test_use_after_close(self):
330330
with self.assertRaisesRegex(InvalidOperation, "Cannot use MongoClient after close"):
331331
client.admin.command("ping")
332332

333-
# Not available for versions of Python without "register_at_fork"
334333
@unittest.skipIf(
335334
not hasattr(os, "register_at_fork"),
336335
"register_at_fork not available in this version of Python",
@@ -342,14 +341,7 @@ def test_use_after_close(self):
342341
def test_fork(self):
343342
opts = AutoEncryptionOpts(KMS_PROVIDERS, "keyvault.datakeys")
344343
client = rs_or_single_client(auto_encryption_opts=opts)
345-
346-
lock_pid = os.fork()
347-
if lock_pid == 0:
348-
client.admin.command("ping")
349-
client.close()
350-
os._exit(0)
351-
else:
352-
self.assertEqual(0, os.waitpid(lock_pid, 0)[1])
344+
with self.fork():
353345
client.admin.command("ping")
354346
client.close()
355347

test/test_fork.py

Lines changed: 36 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import os
1818
from multiprocessing import Pipe
19-
from test import IntegrationTest, client_context
19+
from test import IntegrationTest
2020
from test.utils import (
2121
ExceptionCatchingThread,
2222
is_greenthread_patched,
@@ -27,12 +27,6 @@
2727
from bson.objectid import ObjectId
2828

2929

30-
@client_context.require_connection
31-
def setUpModule():
32-
pass
33-
34-
35-
# Not available for versions of Python without "register_at_fork"
3630
@skipIf(
3731
not hasattr(os, "register_at_fork"), "register_at_fork not available in this version of Python"
3832
)
@@ -42,83 +36,52 @@ def setUpModule():
4236
)
4337
class TestFork(IntegrationTest):
4438
def test_lock_client(self):
45-
"""
46-
Forks the client with some items locked.
47-
Parent => All locks should be as before the fork.
48-
Child => All locks should be reset.
49-
"""
50-
51-
def exit_cond():
52-
self.client.admin.command("ping")
53-
return 0
54-
39+
# Forks the client with some items locked.
40+
# Parent => All locks should be as before the fork.
41+
# Child => All locks should be reset.
5542
with self.client._MongoClient__lock:
56-
# Call _get_topology, will launch a thread to fork upon __enter__ing
57-
# the with region.
58-
lock_pid = os.fork()
59-
# The POSIX standard states only the forking thread is cloned.
60-
# In the parent, it'll return here.
61-
# In the child, it'll end with the calling thread.
62-
if lock_pid == 0:
63-
code = -1
64-
try:
65-
code = exit_cond()
66-
finally:
67-
os._exit(code)
68-
else:
69-
self.assertEqual(0, os.waitpid(lock_pid, 0)[1])
43+
with self.fork() as pid:
44+
if pid == 0: # Child
45+
self.client.admin.command("ping")
46+
self.client.admin.command("ping")
7047

7148
def test_lock_object_id(self):
72-
"""
73-
Forks the client with ObjectId's _inc_lock locked.
74-
Parent => _inc_lock should remain locked.
75-
Child => _inc_lock should be unlocked.
76-
"""
49+
# Forks the client with ObjectId's _inc_lock locked.
50+
# Parent => _inc_lock should remain locked.
51+
# Child => _inc_lock should be unlocked.
7752
with ObjectId._inc_lock:
78-
lock_pid: int = os.fork()
79-
80-
if lock_pid == 0:
81-
code = -1
82-
try:
83-
code = int(ObjectId._inc_lock.locked())
84-
finally:
85-
os._exit(code)
86-
else:
87-
self.assertEqual(0, os.waitpid(lock_pid, 0)[1])
53+
with self.fork() as pid:
54+
if pid == 0: # Child
55+
self.assertFalse(ObjectId._inc_lock.locked())
56+
self.assertTrue(ObjectId())
8857

8958
def test_topology_reset(self):
90-
"""
91-
Tests that topologies are different from each other.
92-
Cannot use ID because virtual memory addresses may be the same.
93-
Cannot reinstantiate ObjectId in the topology settings.
94-
Relies on difference in PID when opened again.
95-
"""
59+
# Tests that topologies are different from each other.
60+
# Cannot use ID because virtual memory addresses may be the same.
61+
# Cannot reinstantiate ObjectId in the topology settings.
62+
# Relies on difference in PID when opened again.
9663
parent_conn, child_conn = Pipe()
9764
init_id = self.client._topology._pid
9865
parent_cursor_exc = self.client._kill_cursors_executor
99-
lock_pid: int = os.fork()
100-
101-
if lock_pid == 0: # Child
102-
self.client.admin.command("ping")
103-
child_conn.send(self.client._topology._pid)
104-
child_conn.send(
105-
(
106-
parent_cursor_exc != self.client._kill_cursors_executor,
107-
"client._kill_cursors_executor was not reinitialized",
66+
with self.fork() as pid:
67+
if pid == 0: # Child
68+
self.client.admin.command("ping")
69+
child_conn.send(self.client._topology._pid)
70+
child_conn.send(
71+
(
72+
parent_cursor_exc != self.client._kill_cursors_executor,
73+
"client._kill_cursors_executor was not reinitialized",
74+
)
10875
)
109-
)
110-
os._exit(0)
111-
else: # Parent
112-
self.assertEqual(0, os.waitpid(lock_pid, 0)[1])
113-
self.assertEqual(self.client._topology._pid, init_id)
114-
child_id = parent_conn.recv()
115-
self.assertNotEqual(child_id, init_id)
116-
passed, msg = parent_conn.recv()
117-
self.assertTrue(passed, msg)
76+
else: # Parent
77+
self.assertEqual(self.client._topology._pid, init_id)
78+
child_id = parent_conn.recv()
79+
self.assertNotEqual(child_id, init_id)
80+
passed, msg = parent_conn.recv()
81+
self.assertTrue(passed, msg)
11882

11983
def test_many_threaded(self):
12084
# Fork randomly while doing operations.
121-
12285
clients = []
12386
for _ in range(10):
12487
c = rs_or_single_client()
@@ -143,17 +106,10 @@ def action(client):
143106
rc = self.clients[i % len(self.clients)]
144107
if i % 50 == 0 and self.fork:
145108
# Fork
146-
pid = os.fork()
147-
if pid == 0:
148-
code = -1
149-
try:
109+
with self.runner.fork() as pid:
110+
if pid == 0: # Child
150111
for c in self.clients:
151112
action(c)
152-
code = 0
153-
finally:
154-
os._exit(code)
155-
else:
156-
self.runner.assertEqual(0, os.waitpid(pid, 0)[1])
157113
action(rc)
158114

159115
threads = [ForkThread(self, clients) for _ in range(10)]

0 commit comments

Comments
 (0)