Skip to content

run: Support system signal as a coverage report dump trigger. #1998

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion coverage/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
import os
import os.path
import shlex
import signal
import sys
import textwrap
import traceback
import types

from typing import cast, Any, NoReturn

Expand Down Expand Up @@ -188,6 +190,16 @@ class Opts:
"'pyproject.toml' are tried. [env: COVERAGE_RCFILE]"
),
)
save_signal = optparse.make_option(
'', '--save-signal', action='store', metavar='SAVE_SIGNAL',
choices = ['USR1', 'USR2'],
help=(
"Define a system signal that will trigger coverage report save operation. " +
"It is important that target script do not intercept this signal. " +
"Currently supported options are: USR1, USR2. " +
"This feature does not work on Windows."
),
)
show_contexts = optparse.make_option(
"--show-contexts", action="store_true",
help="Show contexts for covered lines.",
Expand Down Expand Up @@ -228,7 +240,6 @@ class Opts:
help="Display version information and exit.",
)


class CoverageOptionParser(optparse.OptionParser):
"""Base OptionParser for coverage.py.

Expand Down Expand Up @@ -264,6 +275,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
pylib=None,
quiet=None,
rcfile=True,
save_signal=None,
show_contexts=None,
show_missing=None,
skip_covered=None,
Expand Down Expand Up @@ -523,6 +535,7 @@ def get_prog_name(self) -> str:
Opts.omit,
Opts.pylib,
Opts.parallel_mode,
Opts.save_signal,
Opts.source,
Opts.timid,
] + GLOBAL_ARGS,
Expand Down Expand Up @@ -807,6 +820,11 @@ def do_help(

return False

def do_signal_save(self, _signum: int, _frame: types.FrameType | None) -> None:
""" Signal handler to save coverage report """
print("Saving coverage data ...")
self.coverage.save()

def do_run(self, options: optparse.Values, args: list[str]) -> int:
"""Implementation of 'coverage run'."""

Expand Down Expand Up @@ -851,6 +869,18 @@ def do_run(self, options: optparse.Values, args: list[str]) -> int:
if options.append:
self.coverage.load()

if options.save_signal:
if env.WINDOWS:
show_help("Signals are not supported in Windows environment.")
return ERR
if options.save_signal.upper() == 'USR1':
signal.signal(signal.SIGUSR1, self.do_signal_save)
elif options.save_signal.upper() == 'USR2':
signal.signal(signal.SIGUSR2, self.do_signal_save)
else:
show_help(f"Unsupported signal for save coverage report: {options.save_signal}")
return ERR

# Run the script.
self.coverage.start()
code_ran = True
Expand Down
11 changes: 10 additions & 1 deletion doc/cmd.rst
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ There are many options:
-p, --parallel-mode Append the machine name, process id and random number
to the data file name to simplify collecting data from
many processes.
--save-signal=SAVE_SIGNAL
Define a system signal that will trigger coverage
report save operation. It is important that target
script do not intercept this signal. Currently
supported options are: USR1, USR2. This feature does
not work on Windows.
--source=SRC1,SRC2,...
A list of directories or importable names of code to
measure.
Expand All @@ -143,7 +149,7 @@ There are many options:
--rcfile=RCFILE Specify configuration file. By default '.coveragerc',
'setup.cfg', 'tox.ini', and 'pyproject.toml' are
tried. [env: COVERAGE_RCFILE]
.. [[[end]]] (sum: saD//ido/B)
.. [[[end]]] (sum: X8Kbvdq2+f)

If you want :ref:`branch coverage <branch>` measurement, use the ``--branch``
flag. Otherwise only statement coverage is measured.
Expand Down Expand Up @@ -215,6 +221,9 @@ and may change in the future.
These options can also be set in the :ref:`config_run` section of your
.coveragerc file.

In case if you are specifying ``--dump_signal``, please make sure that
your target script doesn't intercept this signal. Otherwise the coverage
reports will not be generated.

.. _cmd_warnings:

Expand Down
45 changes: 45 additions & 0 deletions tests/test_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,51 @@ def test_module_name(self) -> None:
out = self.run_command("python -m coverage")
assert "Use 'coverage help' for help" in out

@pytest.mark.skipif(env.WINDOWS, reason="This test is not for Windows")
def test_save_signal(self) -> None:
test_file = "dummy_hello.py"
self.assert_doesnt_exist(".coverage")
self.make_file(test_file, """\
import os
import signal

print(f"Sending SIGUSR1 to process {os.getpid()}")
os.kill(os.getpid(), signal.SIGUSR1)
os.kill(os.getpid(), signal.SIGKILL)

print('Done and goodbye')
""")
covered_lines = 4
self.run_command(f"coverage run --save-signal USR1 {test_file}")
self.assert_exists(".coverage")
data = coverage.CoverageData()
data.read()
assert line_counts(data)[test_file] == covered_lines
out = self.run_command("coverage report")
assert out == textwrap.dedent("""\
Name Stmts Miss Cover
------------------------------------
dummy_hello.py 6 2 67%
------------------------------------
TOTAL 6 2 67%
""")

# Negative test for signal
@pytest.mark.skipif(env.WINDOWS, reason="This test is not for Windows")
def test_save_signal_no_send(self) -> None:
test_file = "dummy_hello.py"
self.assert_doesnt_exist(".coverage")
self.make_file(test_file, """\
import os
import signal

os.kill(os.getpid(), signal.SIGKILL)

print('Done and goodbye')
""")
self.run_command(f"coverage run --save-signal USR1 {test_file}")
self.assert_doesnt_exist(".coverage")


TRY_EXECFILE = os.path.join(os.path.dirname(__file__), "modules/process_test/try_execfile.py")

Expand Down
Loading