Skip to content

Commit 6f97f0a

Browse files
plfjitsankitkp
andcommitted
gh-103758: Test that os.register_at_fork handlers accumulate across forks
Add a test verifying that handlers registered after an earlier fork() run together with previously registered handlers in subsequent forks, preserving LIFO order for 'before' handlers and FIFO order for 'after_*' handlers. Co-authored-by: Ankit Kumar Pandey <itsankitkp@users.noreply.github.com>
1 parent 036d309 commit 6f97f0a

1 file changed

Lines changed: 68 additions & 0 deletions

File tree

‎Lib/test/test_os/test_posix.py‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,74 @@ def test_register_at_fork(self):
296296
"""
297297
assert_python_ok('-c', code)
298298

299+
@support.requires_fork()
300+
def test_register_at_fork_accumulates_across_forks(self):
301+
# gh-103758: handlers registered after an earlier fork() must run
302+
# together with the earlier ones. 'before' handlers run in reverse
303+
# registration order, 'after_in_parent' and 'after_in_child' in
304+
# registration order.
305+
#
306+
# Run in a subprocess: fork handlers cannot be unregistered.
307+
# Every process checks its own copy of 'events', so there is no
308+
# shared pipe; the child reports through its exit status only.
309+
code = """if 1:
310+
import os
311+
import sys
312+
import traceback
313+
from test.support import wait_process
314+
315+
events = []
316+
317+
def recorder(tag):
318+
return lambda: events.append(tag)
319+
320+
# Each stage: handlers registered before that fork, then the
321+
# expected events in the parent and in the child.
322+
stages = [
323+
# Stage 1: first registration only.
324+
(dict(before=recorder('before1'),
325+
after_in_parent=recorder('parent1'),
326+
after_in_child=recorder('child1')),
327+
['before1', 'parent1'],
328+
['before1', 'child1']),
329+
# Stage 2: added after the first fork; no after_in_parent.
330+
(dict(before=recorder('before2'),
331+
after_in_child=recorder('child2')),
332+
['before2', 'before1', 'parent1'],
333+
['before2', 'before1', 'child1', 'child2']),
334+
# Stage 3: added after the second fork.
335+
(dict(before=recorder('before3'),
336+
after_in_parent=recorder('parent3'),
337+
after_in_child=recorder('child3')),
338+
['before3', 'before2', 'before1', 'parent1', 'parent3'],
339+
['before3', 'before2', 'before1',
340+
'child1', 'child2', 'child3']),
341+
]
342+
343+
for n, (handlers, want_parent, want_child) in enumerate(stages, 1):
344+
os.register_at_fork(**handlers)
345+
events.clear()
346+
pid = os.fork()
347+
if pid == 0:
348+
# Child: os._exit() skips atexit hooks, the parent's
349+
# finally blocks and buffer flushing.
350+
exitcode = 2
351+
try:
352+
if events == want_child:
353+
exitcode = 0
354+
else:
355+
exitcode = 1
356+
print(f'stage {n}: child events {events!r}, '
357+
f'expected {want_child!r}', file=sys.stderr)
358+
except BaseException:
359+
traceback.print_exc()
360+
finally:
361+
sys.stderr.flush()
362+
os._exit(exitcode)
363+
wait_process(pid, exitcode=0)
364+
assert events == want_parent, (n, events, want_parent)
365+
"""
366+
assert_python_ok('-c', code)
299367
@unittest.skipUnless(hasattr(posix, 'lockf'), "test needs posix.lockf()")
300368
def test_lockf(self):
301369
fd = os.open(os_helper.TESTFN, os.O_WRONLY | os.O_CREAT)

0 commit comments

Comments
 (0)