Skip to content

Commit ca87161

Browse files
ark-gnedbat
authored andcommitted
feat: system signal as a coverage data save trigger
Signed-off-by: Arkady Gilinsky <[email protected]> misc: Changes per pull request * Set options in alphabetical order * Rename "dump-signal" to "save-signal" Signed-off-by: Arkady Gilinsky <[email protected]> docs: Update - save signal doesn't work on Windows Signed-off-by: Arkady Gilinsky <[email protected]> test: Add test cases for signal dump Signed-off-by: Arkady Gilinsky <[email protected]> docs: Fix docs Signed-off-by: Arkady Gilinsky <[email protected]> test: Remove redundant test Signed-off-by: Arkady Gilinsky <[email protected]>
1 parent 1c7b5e0 commit ca87161

File tree

3 files changed

+70
-2
lines changed

3 files changed

+70
-2
lines changed

coverage/cmdline.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
import os
1111
import os.path
1212
import shlex
13+
import signal
1314
import sys
1415
import textwrap
1516
import traceback
17+
import types
1618

1719
from typing import cast, Any, NoReturn
1820

@@ -188,6 +190,16 @@ class Opts:
188190
"'pyproject.toml' are tried. [env: COVERAGE_RCFILE]"
189191
),
190192
)
193+
save_signal = optparse.make_option(
194+
'', '--save-signal', action='store', metavar='SAVE_SIGNAL',
195+
choices = ['USR1', 'USR2'],
196+
help=(
197+
"Define a system signal that will trigger coverage report save operation. " +
198+
"It is important that target script do not intercept this signal. " +
199+
"Currently supported options are: USR1, USR2. " +
200+
"This feature does not work on Windows."
201+
),
202+
)
191203
show_contexts = optparse.make_option(
192204
"--show-contexts", action="store_true",
193205
help="Show contexts for covered lines.",
@@ -228,7 +240,6 @@ class Opts:
228240
help="Display version information and exit.",
229241
)
230242

231-
232243
class CoverageOptionParser(optparse.OptionParser):
233244
"""Base OptionParser for coverage.py.
234245
@@ -264,6 +275,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
264275
pylib=None,
265276
quiet=None,
266277
rcfile=True,
278+
save_signal=None,
267279
show_contexts=None,
268280
show_missing=None,
269281
skip_covered=None,
@@ -523,6 +535,7 @@ def get_prog_name(self) -> str:
523535
Opts.omit,
524536
Opts.pylib,
525537
Opts.parallel_mode,
538+
Opts.save_signal,
526539
Opts.source,
527540
Opts.timid,
528541
] + GLOBAL_ARGS,
@@ -807,6 +820,11 @@ def do_help(
807820

808821
return False
809822

823+
def do_signal_save(self, _signum: int, _frame: types.FrameType | None) -> None:
824+
""" Signal handler to save coverage report """
825+
print("Saving coverage data ...")
826+
self.coverage.save()
827+
810828
def do_run(self, options: optparse.Values, args: list[str]) -> int:
811829
"""Implementation of 'coverage run'."""
812830

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

872+
if options.save_signal:
873+
if env.WINDOWS:
874+
show_help("Signals are not supported in Windows environment.")
875+
return ERR
876+
if options.save_signal.upper() == 'USR1':
877+
signal.signal(signal.SIGUSR1, self.do_signal_save)
878+
elif options.save_signal.upper() == 'USR2':
879+
signal.signal(signal.SIGUSR2, self.do_signal_save)
880+
else:
881+
show_help(f"Unsupported signal for save coverage report: {options.save_signal}")
882+
return ERR
883+
854884
# Run the script.
855885
self.coverage.start()
856886
code_ran = True

doc/cmd.rst

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ There are many options:
133133
-p, --parallel-mode Append the machine name, process id and random number
134134
to the data file name to simplify collecting data from
135135
many processes.
136+
--save-signal=SAVE_SIGNAL
137+
Define a system signal that will trigger coverage
138+
report save operation. It is important that target
139+
script do not intercept this signal. Currently
140+
supported options are: USR1, USR2. This feature does
141+
not work on Windows.
136142
--source=SRC1,SRC2,...
137143
A list of directories or importable names of code to
138144
measure.
@@ -143,7 +149,7 @@ There are many options:
143149
--rcfile=RCFILE Specify configuration file. By default '.coveragerc',
144150
'setup.cfg', 'tox.ini', and 'pyproject.toml' are
145151
tried. [env: COVERAGE_RCFILE]
146-
.. [[[end]]] (sum: saD//ido/B)
152+
.. [[[end]]] (sum: X8Kbvdq2+f)
147153
148154
If you want :ref:`branch coverage <branch>` measurement, use the ``--branch``
149155
flag. Otherwise only statement coverage is measured.
@@ -215,6 +221,9 @@ and may change in the future.
215221
These options can also be set in the :ref:`config_run` section of your
216222
.coveragerc file.
217223

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

219228
.. _cmd_warnings:
220229

tests/test_process.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,35 @@ def test_module_name(self) -> None:
687687
out = self.run_command("python -m coverage")
688688
assert "Use 'coverage help' for help" in out
689689

690+
@pytest.mark.skipif(env.WINDOWS, reason="This test is not for Windows")
691+
def test_save_signal(self) -> None:
692+
test_file = "dummy_hello.py"
693+
self.assert_doesnt_exist(".coverage")
694+
self.make_file(test_file, """\
695+
import os
696+
import signal
697+
698+
print(f"Sending SIGUSR1 to process {os.getpid()}")
699+
os.kill(os.getpid(), signal.SIGUSR1)
700+
os.kill(os.getpid(), signal.SIGKILL)
701+
702+
print('Done and goodbye')
703+
""")
704+
covered_lines = 4
705+
self.run_command(f"coverage run --save-signal USR1 {test_file}")
706+
self.assert_exists(".coverage")
707+
data = coverage.CoverageData()
708+
data.read()
709+
assert line_counts(data)[test_file] == covered_lines
710+
out = self.run_command("coverage report")
711+
assert out == textwrap.dedent("""\
712+
Name Stmts Miss Cover
713+
------------------------------------
714+
dummy_hello.py 6 2 67%
715+
------------------------------------
716+
TOTAL 6 2 67%
717+
""")
718+
690719

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

0 commit comments

Comments
 (0)