如何从单元测试中模拟终端CTRL + C事件?

时间:2015-08-15 10:14:46

标签: python unit-testing signals sigint

我有一个忽略multiprocessing.Process的{​​{1}}子类:

SIGINT

我不希望在按下 CTRL + C 时终止此过程,所以我试图通过发送{{{}来尝试在我的单元测试中模拟此终端事件1}}发信号通知此进程ID:

# inside the run method
signal.signal(signal.SIGINT, signal.SIG_IGN)

但即使没有忽略这个信号,这个过程也没有终止,所以这个测试没用,我从其他问题中发现了 CTRL + C 事件终端将SIGINT发送到进程组ID,但我不能这样做,因为它也会终止unittest进程。

那么为什么当该进程从os.kill(PID, signal.SIGINT) 收到SIGINT时,该进程不会终止?我应该以另一种方式这样做吗?

2 个答案:

答案 0 :(得分:3)

子进程应在收到SIGINT时终止,除非它忽略该信号或安装了自己的处理程序。如果您没有明确忽略子节点中的SIGINT,则可能在父节点中忽略SIGINT,因此在子节点中忽略SIGINT,因为信号处置是继承的。

但是,我无法复制您的问题,事实上,我发现了相反的问题:子进程终止,无论其信号处理如何。

如果信号发送得太快,在子进程忽略SIGINT(在其run()方法中)之前,它将被终止。以下是一些演示此问题的代码:

import os, time, signal
from multiprocessing import Process

class P(Process):
    def run(self):
        signal.signal(signal.SIGINT, signal.SIG_IGN)
        return super(P, self).run()

def f():
    print 'Child sleeping...'
    time.sleep(10)
    print 'Child done'

p = P(target=f)
p.start()    
print 'Child started with PID', p.pid

print 'Killing child'
os.kill(p.pid, signal.SIGINT)
print 'Joining child'
p.join()

<强>输出

Child started with PID 1515
Killing child
Joining child
Traceback (most recent call last):
  File "p1.py", line 15, in 
    p.start()    
  File "/usr/lib64/python2.7/multiprocessing/process.py", line 130, in start
    self._popen = Popen(self)
  File "/usr/lib64/python2.7/multiprocessing/forking.py", line 126, in __init__
    code = process_obj._bootstrap()
  File "/usr/lib64/python2.7/multiprocessing/process.py", line 242, in _bootstrap
    from . import util
KeyboardInterrupt

在向孩子发送SIGINT信号之前在父母中添加time.sleep(0.1)的小延迟将解决问题。这将为孩子提供足够的时间来执行忽略SIGINT的run()方法。现在,孩子将忽略该信号:

Child started with PID 1589
Killing child
Child sleeping...
Joining child
Child done

不需要延迟或自定义run()方法的替代方法是将父级设置为忽略SIGINT,启动子级,然后恢复父级的原始SIGINT处理程序。因为信号处理是继承的,所以孩子从开始的那一刻起就会忽略SIGINT:

import os, time, signal
from multiprocessing import Process

def f():
    print 'Child sleeping...'
    time.sleep(10)
    print 'Child done'

p = Process(target=f)
old_sigint = signal.signal(signal.SIGINT, signal.SIG_IGN)
p.start()    
signal.signal(signal.SIGINT, old_sigint)    # restore parent's handler
print 'Child started with PID', p.pid

print 'Killing child'
os.kill(p.pid, signal.SIGINT)
print 'Joining child'
p.join()

<强>输出

Child started with PID 1660
Killing child
Joining child
Child sleeping...
Child done

答案 1 :(得分:2)

该问题的简化版本是:

import os, time, signal

childpid = os.fork()

if childpid == 0:
    # in the child
    time.sleep(5)      # will be interrupted by KeyboardInterrupt
    print "stop child"
else:
    # in the parent
    #time.sleep(1)
    os.kill(childpid, signal.SIGINT)

如果父节点在发送信号之前执行sleep(1),则一切都按预期工作:子节点(并且只有子节点)收到Python KeyboardInterrupt异常,该异常会中断sleep(5)。但是,如果我们在上面的示例中注释sleep(1)kill()似乎完全被忽略:子项运行,睡眠5秒,最后打印"stop child"。因此,您的测试套件可以采用简单的解决方法:只需添加一个小sleep()

据我所知,出现以下(不好)原因:查看CPython源代码,在系统调用fork()之后,子进程显式清除挂起信号列表。但是下面的情况似乎经常发生:父母继续略微超过孩子,并发送SIGINT信号。孩子接收它,但此时它仍然只是在系统调用fork()之后不久,在_clear_pending_signals()之前。结果,信号丢失了。

如果您想在http://bugs.python.org上提交问题,可以将此视为CPython错误。请参阅signalmodule.c中的PyOS_AfterFork()。