Skip to content

Commit 2409d67

Browse files
committed
fix: save data on SIGTERM #1307
This covers multiprocessing.Process.terminate(), and maybe other cases also.
1 parent 53b99ff commit 2409d67

File tree

3 files changed

+84
-3
lines changed

3 files changed

+84
-3
lines changed

coverage/control.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import os
1010
import os.path
1111
import platform
12+
import signal
1213
import sys
1314
import time
1415
import warnings
@@ -228,6 +229,7 @@ def __init__(
228229
self._exclude_re = None
229230
self._debug = None
230231
self._file_mapper = None
232+
self._old_sigterm = None
231233

232234
# State machine variables:
233235
# Have we initialized everything?
@@ -526,6 +528,7 @@ def _init_for_start(self):
526528
self._should_write_debug = True
527529

528530
atexit.register(self._atexit)
531+
self._old_sigterm = signal.signal(signal.SIGTERM, self._on_sigterm)
529532

530533
def _init_data(self, suffix):
531534
"""Create a data file if we don't have one yet."""
@@ -583,15 +586,21 @@ def stop(self):
583586
self._collector.stop()
584587
self._started = False
585588

586-
def _atexit(self):
589+
def _atexit(self, event="atexit"):
587590
"""Clean up on process shutdown."""
588591
if self._debug.should("process"):
589-
self._debug.write(f"atexit: pid: {os.getpid()}, instance: {self!r}")
592+
self._debug.write(f"{event}: pid: {os.getpid()}, instance: {self!r}")
590593
if self._started:
591594
self.stop()
592595
if self._auto_save:
593596
self.save()
594597

598+
def _on_sigterm(self, signum_unused, frame_unused):
599+
"""A handler for signal.SIGTERM."""
600+
self._atexit("sigterm")
601+
signal.signal(signal.SIGTERM, self._old_sigterm)
602+
os.kill(os.getpid(), signal.SIGTERM)
603+
595604
def erase(self):
596605
"""Erase previously collected coverage data.
597606

coverage/multiproc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def _bootstrap(self, *args, **kwargs):
2727
"""Wrapper around _bootstrap to start coverage."""
2828
try:
2929
from coverage import Coverage # avoid circular import
30-
cov = Coverage(data_suffix=True)
30+
cov = Coverage(data_suffix=True, auto_data=True)
3131
cov._warn_preimported_source = False
3232
cov.start()
3333
debug = cov._debug

tests/test_concurrency.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,3 +693,75 @@ def random_load(): # pragma: nested
693693
finally:
694694
os.chdir(old_dir)
695695
should_run[0] = False
696+
697+
698+
class SigtermTest(CoverageTest):
699+
"""Tests of our handling of SIGTERM."""
700+
701+
def test_sigterm_saves_data(self):
702+
# A terminated process should save its coverage data.
703+
self.make_file("clobbered.py", """\
704+
import multiprocessing
705+
import time
706+
707+
def subproc(x):
708+
if x.value == 3:
709+
print("THREE", flush=True) # line 6, missed
710+
else:
711+
print("NOT THREE", flush=True)
712+
x.value = 0
713+
time.sleep(60)
714+
715+
if __name__ == "__main__":
716+
print("START", flush=True)
717+
x = multiprocessing.Value("L", 1)
718+
proc = multiprocessing.Process(target=subproc, args=(x,))
719+
proc.start()
720+
while x.value != 0:
721+
time.sleep(.05)
722+
proc.terminate()
723+
print("END", flush=True)
724+
""")
725+
self.make_file(".coveragerc", """\
726+
[run]
727+
parallel = True
728+
concurrency = multiprocessing
729+
""")
730+
out = self.run_command("coverage run clobbered.py")
731+
assert out == "START\nNOT THREE\nEND\n"
732+
self.run_command("coverage combine")
733+
out = self.run_command("coverage report -m")
734+
assert self.squeezed_lines(out)[2] == "clobbered.py 17 1 94% 6"
735+
736+
def test_sigterm_still_runs(self):
737+
# A terminated process still runs its own SIGTERM handler.
738+
self.make_file("handler.py", """\
739+
import multiprocessing
740+
import signal
741+
import time
742+
743+
def subproc(x):
744+
print("START", flush=True)
745+
def on_sigterm(signum, frame):
746+
print("SIGTERM", flush=True)
747+
748+
signal.signal(signal.SIGTERM, on_sigterm)
749+
x.value = 0
750+
time.sleep(.1)
751+
print("END", flush=True)
752+
753+
if __name__ == "__main__":
754+
x = multiprocessing.Value("L", 1)
755+
proc = multiprocessing.Process(target=subproc, args=(x,))
756+
proc.start()
757+
while x.value != 0:
758+
time.sleep(.02)
759+
proc.terminate()
760+
""")
761+
self.make_file(".coveragerc", """\
762+
[run]
763+
parallel = True
764+
concurrency = multiprocessing
765+
""")
766+
out = self.run_command("coverage run handler.py")
767+
assert out == "START\nSIGTERM\nEND\n"

0 commit comments

Comments
 (0)