Commit 8d64248c authored by Victor Stinner's avatar Victor Stinner

Issue #12363: improve siginterrupt() tests

Backport commits 968b9ff9a059 and aff0a7b0cb12 from the default branch to 3.2
branch. Extract of the changelog messages:

"The previous tests used time.sleep() to synchronize two processes. If the host
was too slow, the test could fail.

The new tests only use one process, but they use a subprocess to:

- have only one thread
- have a timeout on the blocking read (select cannot be used in the test,
select always fail with EINTR, the kernel doesn't restart it)
- not touch signal handling of the parent process"

and

"Add a basic synchronization code between the child and the parent processes:
the child writes "ready" to stdout."

I replaced .communicate(timeout=3.0) by an explicit waiting loop using
Popen.poll().
parent 26d31869
import unittest import errno
from test import support
from contextlib import closing
import gc import gc
import os
import pickle import pickle
import select import select
import signal import signal
import subprocess import subprocess
import sys
import time
import traceback import traceback
import sys, os, time, errno import unittest
from test import support
from contextlib import closing
from test.script_helper import spawn_python
if sys.platform in ('os2', 'riscos'): if sys.platform in ('os2', 'riscos'):
raise unittest.SkipTest("Can't test signal on %s" % sys.platform) raise unittest.SkipTest("Can't test signal on %s" % sys.platform)
...@@ -276,103 +280,96 @@ class WakeupSignalTests(unittest.TestCase): ...@@ -276,103 +280,96 @@ class WakeupSignalTests(unittest.TestCase):
@unittest.skipIf(sys.platform == "win32", "Not valid on Windows") @unittest.skipIf(sys.platform == "win32", "Not valid on Windows")
class SiginterruptTest(unittest.TestCase): class SiginterruptTest(unittest.TestCase):
def setUp(self): def readpipe_interrupted(self, interrupt):
"""Install a no-op signal handler that can be set to allow
interrupts or not, and arrange for the original signal handler to be
re-installed when the test is finished.
"""
self.signum = signal.SIGUSR1
oldhandler = signal.signal(self.signum, lambda x,y: None)
self.addCleanup(signal.signal, self.signum, oldhandler)
def readpipe_interrupted(self):
"""Perform a read during which a signal will arrive. Return True if the """Perform a read during which a signal will arrive. Return True if the
read is interrupted by the signal and raises an exception. Return False read is interrupted by the signal and raises an exception. Return False
if it returns normally. if it returns normally.
""" """
# Create a pipe that can be used for the read. Also clean it up class Timeout(Exception):
# when the test is over, since nothing else will (but see below for pass
# the write end).
r, w = os.pipe() # use a subprocess to have only one thread, to have a timeout on the
self.addCleanup(os.close, r) # blocking read and to not touch signal handling in this process
code = """if 1:
# Create another process which can send a signal to this one to try import errno
# to interrupt the read. import os
ppid = os.getpid() import signal
pid = os.fork() import sys
if pid == 0: interrupt = %r
# Child code: sleep to give the parent enough time to enter the r, w = os.pipe()
# read() call (there's a race here, but it's really tricky to
# eliminate it); then signal the parent process. Also, sleep def handler(signum, frame):
# again to make it likely that the signal is delivered to the pass
# parent process before the child exits. If the child exits
# first, the write end of the pipe will be closed and the test print("ready")
# is invalid. sys.stdout.flush()
try:
time.sleep(0.2) signal.signal(signal.SIGALRM, handler)
os.kill(ppid, self.signum) if interrupt is not None:
time.sleep(0.2) signal.siginterrupt(signal.SIGALRM, interrupt)
finally:
# No matter what, just exit as fast as possible now. # run the test twice
exit_subprocess() for loop in range(2):
else: # send a SIGALRM in a second (during the read)
# Parent code. signal.alarm(1)
# Make sure the child is eventually reaped, else it'll be a try:
# zombie for the rest of the test suite run. # blocking call: read from a pipe without data
self.addCleanup(os.waitpid, pid, 0) os.read(r, 1)
except OSError as err:
# Close the write end of the pipe. The child has a copy, so if err.errno != errno.EINTR:
# it's not really closed until the child exits. We need it to raise
# close when the child exits so that in the non-interrupt case else:
# the read eventually completes, otherwise we could just close sys.exit(2)
# it *after* the test. sys.exit(3)
os.close(w) """ % (interrupt,)
with spawn_python('-c', code) as process:
# Try the read and report whether it is interrupted or not to
# the caller.
try: try:
d = os.read(r, 1) # wait until the child process is loaded and has started
first_line = process.stdout.readline()
# Wait the process with a timeout of 3 seconds
timeout = time.time() + 3.0
while True:
if timeout < time.time():
raise Timeout()
status = process.poll()
if status is not None:
break
time.sleep(0.1)
stdout, stderr = process.communicate()
except Timeout:
process.kill()
return False return False
except OSError as err: else:
if err.errno != errno.EINTR: stdout = first_line + stdout
raise exitcode = process.wait()
return True if exitcode not in (2, 3):
raise Exception("Child error (exit code %s): %s"
% (exitcode, stdout))
return (exitcode == 3)
def test_without_siginterrupt(self): def test_without_siginterrupt(self):
"""If a signal handler is installed and siginterrupt is not called # If a signal handler is installed and siginterrupt is not called
at all, when that signal arrives, it interrupts a syscall that's in # at all, when that signal arrives, it interrupts a syscall that's in
progress. # progress.
""" interrupted = self.readpipe_interrupted(None)
i = self.readpipe_interrupted() self.assertTrue(interrupted)
self.assertTrue(i)
# Arrival of the signal shouldn't have changed anything.
i = self.readpipe_interrupted()
self.assertTrue(i)
def test_siginterrupt_on(self): def test_siginterrupt_on(self):
"""If a signal handler is installed and siginterrupt is called with # If a signal handler is installed and siginterrupt is called with
a true value for the second argument, when that signal arrives, it # a true value for the second argument, when that signal arrives, it
interrupts a syscall that's in progress. # interrupts a syscall that's in progress.
""" interrupted = self.readpipe_interrupted(True)
signal.siginterrupt(self.signum, 1) self.assertTrue(interrupted)
i = self.readpipe_interrupted()
self.assertTrue(i)
# Arrival of the signal shouldn't have changed anything.
i = self.readpipe_interrupted()
self.assertTrue(i)
def test_siginterrupt_off(self): def test_siginterrupt_off(self):
"""If a signal handler is installed and siginterrupt is called with # If a signal handler is installed and siginterrupt is called with
a false value for the second argument, when that signal arrives, it # a false value for the second argument, when that signal arrives, it
does not interrupt a syscall that's in progress. # does not interrupt a syscall that's in progress.
""" interrupted = self.readpipe_interrupted(False)
signal.siginterrupt(self.signum, 0) self.assertFalse(interrupted)
i = self.readpipe_interrupted()
self.assertFalse(i)
# Arrival of the signal shouldn't have changed anything.
i = self.readpipe_interrupted()
self.assertFalse(i)
@unittest.skipIf(sys.platform == "win32", "Not valid on Windows") @unittest.skipIf(sys.platform == "win32", "Not valid on Windows")
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment