Skip to content

Commit f014069

Browse files
committed
Move fork checking into its own file
1 parent dbd9edc commit f014069

2 files changed

Lines changed: 156 additions & 137 deletions

File tree

apsw/tests/__main__.py

Lines changed: 3 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -11976,116 +11976,6 @@ def foo():
1197611976
for k, v in apsw.faultdict.items():
1197711977
assert v is False, f"faultdict {k} never fired"
1197811978

11979-
# This test is run last by deliberate name choice. If it did
11980-
# uncover any bugs there isn't much that can be done to turn the
11981-
# checker off.
11982-
def testzzForkChecker(self):
11983-
"Test detection of using objects across fork"
11984-
# need to free up everything that already exists
11985-
self.db.close()
11986-
self.db = None
11987-
gc.collect()
11988-
# install it
11989-
apsw.fork_checker()
11990-
11991-
# return some objects
11992-
def getstuff():
11993-
db = apsw.Connection(":memory:")
11994-
cur = db.cursor()
11995-
for row in cur.execute(
11996-
"create table foo(x);insert into foo values(1);insert into foo values(x'aabbcc'); select last_insert_rowid()"
11997-
):
11998-
blobid = row[0]
11999-
blob = db.blob_open("main", "foo", "x", blobid, 0)
12000-
db2 = apsw.Connection(":memory:")
12001-
backup = db2.backup("main", db, "main")
12002-
return (db, cur, blob, backup)
12003-
12004-
# test the objects
12005-
def teststuff(db, cur, blob, backup):
12006-
if db:
12007-
db.cursor().execute("select 3")
12008-
if cur:
12009-
cur.execute("select 3")
12010-
if blob:
12011-
blob.read(1)
12012-
if backup:
12013-
backup.step()
12014-
12015-
# Sanity check
12016-
teststuff(*getstuff())
12017-
# get some to use in parent
12018-
parent = getstuff()
12019-
# to be used (and fail with error) in child
12020-
child = getstuff()
12021-
12022-
def childtest(*args):
12023-
# we can't use unittest methods here since we are in a different process
12024-
12025-
# this should work
12026-
teststuff(*getstuff())
12027-
12028-
# ignore the unraisable stuff sent to sys.excepthook
12029-
def eh(*args):
12030-
pass
12031-
12032-
sys.excepthook = eh
12033-
12034-
# call with each separate item to check
12035-
try:
12036-
for i in range(len(args)):
12037-
a = [None] * len(args)
12038-
a[i] = args[i]
12039-
try:
12040-
teststuff(*a)
12041-
except apsw.ForkingViolationError:
12042-
pass
12043-
except apsw.ForkingViolationError:
12044-
# we get one final exception "between" line due to the
12045-
# nature of how the exception is raised
12046-
pass
12047-
# this should work again
12048-
teststuff(*getstuff())
12049-
os._exit(0)
12050-
12051-
suppressWarning("DeprecationWarning") # we are deliberately forking
12052-
pid = os.fork()
12053-
12054-
if pid == 0:
12055-
# child
12056-
counter = 0
12057-
12058-
def ueh(unraisable):
12059-
if unraisable.exc_type != apsw.ForkingViolationError:
12060-
print("\n\nUnraisable exception in child process", unraisable)
12061-
return sys.__unraisablehook__(unraisable)
12062-
nonlocal counter
12063-
counter += 1
12064-
if counter > 100:
12065-
os._exit(0)
12066-
12067-
sys.unraisablehook = ueh
12068-
try:
12069-
childtest(*child)
12070-
except:
12071-
print("\n\nThis exception in THE CHILD PROCESS OF FORK CHECKER\n", file=sys.stderr)
12072-
traceback.print_exc()
12073-
print("\nEnd CHILD traceback\n\n")
12074-
os._exit(1)
12075-
os._exit(0)
12076-
12077-
rc = os.waitpid(pid, 0)
12078-
self.assertEqual(0, os.waitstatus_to_exitcode(rc[1]))
12079-
12080-
teststuff(*parent)
12081-
12082-
# we call shutdown to free mutexes used in fork checker,
12083-
# so clear out all the things first
12084-
del child
12085-
del parent
12086-
gc.collect()
12087-
apsw.shutdown()
12088-
1208911979

1209011980
testtimeout = False # timeout testing adds several seconds to each run
1209111981

@@ -12214,33 +12104,6 @@ def setup():
1221412104
if not getattr(memdb, "enableloadextension", None):
1221512105
del APSW.testLoadExtension
1221612106

12217-
# Fork checker is becoming less usefull on newer Pythons because
12218-
# multiprocessing really doesn't want you to use fork and does
12219-
# alternate methods instead. We also run sanitizers on most
12220-
# recent Python which makes things even more convoluted.
12221-
forkcheck = False
12222-
if (
12223-
hasattr(apsw, "fork_checker")
12224-
and hasattr(os, "fork")
12225-
and platform.python_implementation() != "PyPy"
12226-
and sys.version_info < (3, 13)
12227-
):
12228-
try:
12229-
import multiprocessing
12230-
12231-
if hasattr(multiprocessing, "get_start_method"):
12232-
if multiprocessing.get_start_method() != "fork":
12233-
raise ImportError
12234-
# sometimes the import works but doing anything fails
12235-
val = multiprocessing.Value("i", 0)
12236-
forkcheck = True
12237-
except ImportError:
12238-
pass
12239-
12240-
# we also remove forkchecker if doing multiple iterations
12241-
if not forkcheck or "APSW_TEST_ITERATIONS" in os.environ:
12242-
del ZZFaultInjection.testzzForkChecker
12243-
1224412107
if not is64bit or "APSW_TEST_LARGE" not in os.environ:
1224512108
del APSW.testLargeObjects
1224612109

@@ -12290,6 +12153,9 @@ def setup():
1229012153
from .carray import *
1229112154
from .aiotest import *
1229212155

12156+
if "APSW_TEST_ITERATIONS" not in os.environ:
12157+
from .fork_checker import *
12158+
1229312159
if __name__ == "__main__":
1229412160
setup()
1229512161

apsw/tests/fork_checker.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
#!/usr/bin/env python3
2+
3+
import gc
4+
import unittest
5+
import os
6+
import sys
7+
import platform
8+
import warnings
9+
10+
import apsw
11+
12+
def suppressWarning(name):
13+
if hasattr(__builtins__, name):
14+
warnings.simplefilter("ignore", getattr(__builtins__, name))
15+
16+
# Name deliberate to make it run last
17+
class zzZForkChecker(unittest.TestCase):
18+
19+
def testForkChecker(self):
20+
"Test detection of using objects across fork"
21+
# need to free up everything that already exists
22+
gc.collect()
23+
# install it
24+
apsw.fork_checker()
25+
26+
# return some objects
27+
def getstuff():
28+
db = apsw.Connection(":memory:")
29+
cur = db.cursor()
30+
for row in cur.execute(
31+
"create table foo(x);insert into foo values(1);insert into foo values(x'aabbcc'); select last_insert_rowid()"
32+
):
33+
blobid = row[0]
34+
blob = db.blob_open("main", "foo", "x", blobid, 0)
35+
db2 = apsw.Connection(":memory:")
36+
backup = db2.backup("main", db, "main")
37+
return (db, cur, blob, backup)
38+
39+
# test the objects
40+
def teststuff(db, cur, blob, backup):
41+
if db:
42+
db.cursor().execute("select 3")
43+
if cur:
44+
cur.execute("select 3")
45+
if blob:
46+
blob.read(1)
47+
if backup:
48+
backup.step()
49+
50+
# Sanity check
51+
teststuff(*getstuff())
52+
# get some to use in parent
53+
parent = getstuff()
54+
# to be used (and fail with error) in child
55+
child = getstuff()
56+
57+
def childtest(*args):
58+
# we can't use unittest methods here since we are in a different process
59+
60+
# this should work
61+
teststuff(*getstuff())
62+
63+
# ignore the unraisable stuff sent to sys.excepthook
64+
def eh(*args):
65+
pass
66+
67+
sys.excepthook = eh
68+
69+
# call with each separate item to check
70+
try:
71+
for i in range(len(args)):
72+
a = [None] * len(args)
73+
a[i] = args[i]
74+
try:
75+
teststuff(*a)
76+
except apsw.ForkingViolationError:
77+
pass
78+
except apsw.ForkingViolationError:
79+
# we get one final exception "between" line due to the
80+
# nature of how the exception is raised
81+
pass
82+
# this should work again
83+
teststuff(*getstuff())
84+
os._exit(0)
85+
86+
suppressWarning("DeprecationWarning") # we are deliberately forking
87+
pid = os.fork()
88+
89+
if pid == 0:
90+
# child
91+
counter = 0
92+
93+
def ueh(unraisable):
94+
if unraisable.exc_type != apsw.ForkingViolationError:
95+
print("\n\nUnraisable exception in child process", unraisable)
96+
return sys.__unraisablehook__(unraisable)
97+
nonlocal counter
98+
counter += 1
99+
if counter > 100:
100+
os._exit(0)
101+
102+
sys.unraisablehook = ueh
103+
try:
104+
childtest(*child)
105+
except:
106+
print("\n\nThis exception in THE CHILD PROCESS OF FORK CHECKER\n", file=sys.stderr)
107+
traceback.print_exc()
108+
print("\nEnd CHILD traceback\n\n")
109+
os._exit(1)
110+
os._exit(0)
111+
112+
rc = os.waitpid(pid, 0)
113+
self.assertEqual(0, os.waitstatus_to_exitcode(rc[1]))
114+
115+
teststuff(*parent)
116+
117+
# we call shutdown to free mutexes used in fork checker,
118+
# so clear out all the things first
119+
del child
120+
del parent
121+
gc.collect()
122+
apsw.shutdown()
123+
apsw.initialize()
124+
125+
126+
# Fork checker is becoming less useful on newer Pythons because
127+
# multiprocessing really doesn't want you to use fork and does
128+
# alternate methods instead. We also run sanitizers on most
129+
# recent Python which makes things even more convoluted.
130+
forkcheck = False
131+
132+
if hasattr(apsw, "fork_checker") and hasattr(os, "fork") and platform.python_implementation() != "PyPy" \
133+
and sys.version_info < (3, 13):
134+
try:
135+
import multiprocessing
136+
137+
if hasattr(multiprocessing, "get_start_method"):
138+
if multiprocessing.get_start_method() != "fork":
139+
raise ImportError
140+
# sometimes the import works but doing anything fails
141+
val = multiprocessing.Value("i", 0)
142+
forkcheck = True
143+
except ImportError:
144+
pass
145+
146+
if forkcheck:
147+
__all__ = (zzZForkChecker,)
148+
else:
149+
__all__ = tuple()
150+
151+
if __name__ == '__main__':
152+
if forkcheck:
153+
unittest.main()

0 commit comments

Comments
 (0)