Skip to content

Commit c1e89af

Browse files
committed
Add cleanup_on_signal and allow registering cleanup on multiple signals. Also fix incorrect handling for default handlers of INT/TERM.
1 parent 0cf9aad commit c1e89af

File tree

2 files changed

+183
-10
lines changed

2 files changed

+183
-10
lines changed

src/pytest_cov/embed.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,21 +88,26 @@ def cleanup(cov=None):
8888

8989
multiprocessing_finish = cleanup # in case someone dared to use this internal
9090

91-
_previous_handler = None
91+
_previous_handlers = {}
9292

9393

94-
def _sigterm_handler(num, frame):
94+
def _signal_cleanup_handler(signum, frame):
9595
cleanup()
96+
_previous_handler = _previous_handlers.get(signum)
9697
if _previous_handler == signal.SIG_IGN:
97-
pass
98+
return
9899
elif _previous_handler:
99-
_previous_handler(num, frame)
100-
else:
101-
raise SystemExit(0)
100+
_previous_handler(signum, frame)
101+
elif signum == signal.SIGTERM:
102+
os._exit(128 + signum)
103+
elif signum == signal.SIGINT:
104+
raise KeyboardInterrupt()
102105

103106

104-
def cleanup_on_sigterm():
105-
global _previous_handler
107+
def cleanup_on_signal(signum):
108+
_previous_handlers[signum] = signal.getsignal(signum)
109+
signal.signal(signum, _signal_cleanup_handler)
110+
106111

107-
_previous_handler = signal.getsignal(signal.SIGTERM)
108-
signal.signal(signal.SIGTERM, _sigterm_handler)
112+
def cleanup_on_sigterm():
113+
cleanup_on_signal(signal.SIGTERM)

tests/test_pytest_cov.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -986,6 +986,174 @@ def test_run_target():
986986
])
987987
assert result.ret == 0
988988

989+
990+
@pytest.mark.skipif('sys.platform == "win32"',
991+
reason="fork not available on Windows")
992+
def test_cleanup_on_sigterm(testdir):
993+
script = testdir.makepyfile('''
994+
import os, signal, subprocess, sys, time
995+
996+
def cleanup(num, frame):
997+
print("num == signal.SIGTERM => %s" % (num == signal.SIGTERM))
998+
raise Exception()
999+
1000+
def test_run():
1001+
proc = subprocess.Popen([sys.executable, __file__], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
1002+
time.sleep(1)
1003+
proc.terminate()
1004+
stdout, stderr = proc.communicate()
1005+
assert not stderr
1006+
assert stdout == b"""num == signal.SIGTERM => True
1007+
captured Exception()
1008+
"""
1009+
assert proc.returncode == 0
1010+
1011+
if __name__ == "__main__":
1012+
signal.signal(signal.SIGTERM, cleanup)
1013+
1014+
from pytest_cov.embed import cleanup_on_sigterm
1015+
cleanup_on_sigterm()
1016+
1017+
try:
1018+
time.sleep(10)
1019+
except BaseException as exc:
1020+
print("captured %r" % exc)
1021+
''')
1022+
1023+
result = testdir.runpytest('-vv',
1024+
'--cov=%s' % script.dirpath(),
1025+
'--cov-report=term-missing',
1026+
script)
1027+
1028+
result.stdout.fnmatch_lines([
1029+
'*- coverage: platform *, python * -*',
1030+
'test_cleanup_on_sigterm* 26-27',
1031+
'*1 passed*'
1032+
])
1033+
assert result.ret == 0
1034+
1035+
1036+
@pytest.mark.skipif('sys.platform == "win32"', reason="fork not available on Windows")
1037+
@pytest.mark.parametrize('setup', [
1038+
('signal.signal(signal.SIGTERM, signal.SIG_DFL); cleanup_on_sigterm()', '88% 18-19'),
1039+
('cleanup_on_sigterm()', '88% 18-19'),
1040+
('cleanup()', '75% 16-19'),
1041+
])
1042+
def test_cleanup_on_sigterm_sig_dfl(testdir, setup):
1043+
script = testdir.makepyfile('''
1044+
import os, signal, subprocess, sys, time
1045+
1046+
def test_run():
1047+
proc = subprocess.Popen([sys.executable, __file__], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
1048+
time.sleep(1)
1049+
proc.terminate()
1050+
stdout, stderr = proc.communicate()
1051+
assert not stderr
1052+
assert stdout == b""
1053+
assert proc.returncode in [128 + signal.SIGTERM, -signal.SIGTERM]
1054+
1055+
if __name__ == "__main__":
1056+
from pytest_cov.embed import cleanup_on_sigterm, cleanup
1057+
{0}
1058+
1059+
try:
1060+
time.sleep(10)
1061+
except BaseException as exc:
1062+
print("captured %r" % exc)
1063+
'''.format(setup[0]))
1064+
1065+
result = testdir.runpytest('-vv',
1066+
'--cov=%s' % script.dirpath(),
1067+
'--cov-report=term-missing',
1068+
script)
1069+
1070+
result.stdout.fnmatch_lines([
1071+
'*- coverage: platform *, python * -*',
1072+
'test_cleanup_on_sigterm* %s' % setup[1],
1073+
'*1 passed*'
1074+
])
1075+
assert result.ret == 0
1076+
1077+
1078+
@pytest.mark.skipif('sys.platform == "win32"', reason="fork not available on Windows")
1079+
def test_cleanup_on_sigterm_sig_dfl_sigint(testdir):
1080+
script = testdir.makepyfile('''
1081+
import os, signal, subprocess, sys, time
1082+
1083+
def test_run():
1084+
proc = subprocess.Popen([sys.executable, __file__], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
1085+
time.sleep(1)
1086+
proc.send_signal(signal.SIGINT)
1087+
stdout, stderr = proc.communicate()
1088+
assert not stderr
1089+
assert stdout == b"""captured KeyboardInterrupt()
1090+
"""
1091+
assert proc.returncode == 0
1092+
1093+
if __name__ == "__main__":
1094+
from pytest_cov.embed import cleanup_on_signal
1095+
cleanup_on_signal(signal.SIGINT)
1096+
1097+
try:
1098+
time.sleep(10)
1099+
except BaseException as exc:
1100+
print("captured %r" % exc)
1101+
''')
1102+
1103+
result = testdir.runpytest('-vv',
1104+
'--cov=%s' % script.dirpath(),
1105+
'--cov-report=term-missing',
1106+
script)
1107+
1108+
result.stdout.fnmatch_lines([
1109+
'*- coverage: platform *, python * -*',
1110+
'test_cleanup_on_sigterm* 88% 19-20',
1111+
'*1 passed*'
1112+
])
1113+
assert result.ret == 0
1114+
1115+
@pytest.mark.skipif('sys.platform == "win32"', reason="fork not available on Windows")
1116+
def test_cleanup_on_sigterm_sig_ign(testdir):
1117+
script = testdir.makepyfile('''
1118+
import os, signal, subprocess, sys, time
1119+
1120+
def test_run():
1121+
proc = subprocess.Popen([sys.executable, __file__], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
1122+
time.sleep(1)
1123+
proc.send_signal(signal.SIGINT)
1124+
time.sleep(1)
1125+
proc.terminate()
1126+
stdout, stderr = proc.communicate()
1127+
assert not stderr
1128+
assert stdout == b""
1129+
# it appears signal handling is buggy on python 2?
1130+
if sys.version_info == 3: assert proc.returncode in [128 + signal.SIGTERM, -signal.SIGTERM]
1131+
1132+
if __name__ == "__main__":
1133+
signal.signal(signal.SIGINT, signal.SIG_IGN)
1134+
1135+
from pytest_cov.embed import cleanup_on_signal
1136+
cleanup_on_signal(signal.SIGINT)
1137+
1138+
try:
1139+
time.sleep(10)
1140+
except BaseException as exc:
1141+
print("captured %r" % exc)
1142+
''')
1143+
1144+
result = testdir.runpytest('-vv',
1145+
'--cov=%s' % script.dirpath(),
1146+
'--cov-report=term-missing',
1147+
script)
1148+
1149+
result.stdout.fnmatch_lines([
1150+
'*- coverage: platform *, python * -*',
1151+
'test_cleanup_on_sigterm* 89% 23-24',
1152+
'*1 passed*'
1153+
])
1154+
assert result.ret == 0
1155+
1156+
9891157
MODULE = '''
9901158
def func():
9911159
return 1

0 commit comments

Comments
 (0)