Skip to content

Commit

Permalink
fix: save data on SIGTERM #1307
Browse files Browse the repository at this point in the history
This covers multiprocessing.Process.terminate(), and maybe other cases also.
  • Loading branch information
nedbat committed Jan 23, 2022
1 parent 4f770ca commit 3910100
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 3 deletions.
13 changes: 11 additions & 2 deletions coverage/control.py
Expand Up @@ -9,6 +9,7 @@
import os
import os.path
import platform
import signal
import sys
import time
import warnings
Expand Down Expand Up @@ -228,6 +229,7 @@ def __init__(
self._exclude_re = None
self._debug = None
self._file_mapper = None
self._old_sigterm = None

# State machine variables:
# Have we initialized everything?
Expand Down Expand Up @@ -526,6 +528,7 @@ def _init_for_start(self):
self._should_write_debug = True

atexit.register(self._atexit)
self._old_sigterm = signal.signal(signal.SIGTERM, self._on_sigterm)

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

def _atexit(self):
def _atexit(self, event="atexit"):
"""Clean up on process shutdown."""
if self._debug.should("process"):
self._debug.write(f"atexit: pid: {os.getpid()}, instance: {self!r}")
self._debug.write(f"{event}: pid: {os.getpid()}, instance: {self!r}")
if self._started:
self.stop()
if self._auto_save:
self.save()

def _on_sigterm(self, signum_unused, frame_unused):
"""A handler for signal.SIGTERM."""
self._atexit("sigterm")
signal.signal(signal.SIGTERM, self._old_sigterm)
os.kill(os.getpid(), signal.SIGTERM)

def erase(self):
"""Erase previously collected coverage data.
Expand Down
2 changes: 1 addition & 1 deletion coverage/multiproc.py
Expand Up @@ -27,7 +27,7 @@ def _bootstrap(self, *args, **kwargs):
"""Wrapper around _bootstrap to start coverage."""
try:
from coverage import Coverage # avoid circular import
cov = Coverage(data_suffix=True)
cov = Coverage(data_suffix=True, auto_data=True)
cov._warn_preimported_source = False
cov.start()
debug = cov._debug
Expand Down
72 changes: 72 additions & 0 deletions tests/test_concurrency.py
Expand Up @@ -693,3 +693,75 @@ def random_load(): # pragma: nested
finally:
os.chdir(old_dir)
should_run[0] = False


class SigtermTest(CoverageTest):
"""Tests of our handling of SIGTERM."""

def test_sigterm_saves_data(self):
# A terminated process should save its coverage data.
self.make_file("clobbered.py", """\
import multiprocessing
import time
def subproc(x):
if x.value == 3:
print("THREE", flush=True) # line 6, missed
else:
print("NOT THREE", flush=True)
x.value = 0
time.sleep(60)
if __name__ == "__main__":
print("START", flush=True)
x = multiprocessing.Value("L", 1)
proc = multiprocessing.Process(target=subproc, args=(x,))
proc.start()
while x.value != 0:
time.sleep(.05)
proc.terminate()
print("END", flush=True)
""")
self.make_file(".coveragerc", """\
[run]
parallel = True
concurrency = multiprocessing
""")
out = self.run_command("coverage run clobbered.py")
assert out == "START\nNOT THREE\nEND\n"
self.run_command("coverage combine")
out = self.run_command("coverage report -m")
assert self.squeezed_lines(out)[2] == "clobbered.py 17 1 94% 6"

def test_sigterm_still_runs(self):
# A terminated process still runs its own SIGTERM handler.
self.make_file("handler.py", """\
import multiprocessing
import signal
import time
def subproc(x):
print("START", flush=True)
def on_sigterm(signum, frame):
print("SIGTERM", flush=True)
signal.signal(signal.SIGTERM, on_sigterm)
x.value = 0
time.sleep(.1)
print("END", flush=True)
if __name__ == "__main__":
x = multiprocessing.Value("L", 1)
proc = multiprocessing.Process(target=subproc, args=(x,))
proc.start()
while x.value != 0:
time.sleep(.02)
proc.terminate()
""")
self.make_file(".coveragerc", """\
[run]
parallel = True
concurrency = multiprocessing
""")
out = self.run_command("coverage run handler.py")
assert out == "START\nSIGTERM\nEND\n"

0 comments on commit 3910100

Please sign in to comment.