From 35233a5efc69f9c1a274b61f386a551c9a691b54 Mon Sep 17 00:00:00 2001 From: Arkady Gilinsky <9481855+ark-g@users.noreply.github.com> Date: Sat, 12 Jul 2025 07:32:18 +0300 Subject: [PATCH 1/4] run: Support system signal as a coverage report dump trigger. Signed-off-by: Arkady Gilinsky <9481855+ark-g@users.noreply.github.com> --- coverage/cmdline.py | 28 +++++++++++++++++++++++++++- doc/cmd.rst | 10 +++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 998e6b398..480ced404 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -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 @@ -227,7 +229,15 @@ class Opts: "", "--version", action="store_true", help="Display version information and exit.", ) - + dump_signal = optparse.make_option( + '', '--dump_signal', action='store', metavar='DUMP_SIGNAL', + choices = ['USR1', 'USR2'], + help=( + "Define a system signal that will trigger coverage report dump. " + + "It is important that target script do not intercept this signal. " + + "Currently supported options are: USR1, USR2." + ), + ) class CoverageOptionParser(optparse.OptionParser): """Base OptionParser for coverage.py. @@ -251,6 +261,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: data_file=None, debug=None, directory=None, + dump_signal=None, fail_under=None, format=None, help=None, @@ -525,6 +536,7 @@ def get_prog_name(self) -> str: Opts.parallel_mode, Opts.source, Opts.timid, + Opts.dump_signal, ] + GLOBAL_ARGS, usage="[options] [program options]", description="Run a Python program, measuring code execution.", @@ -807,6 +819,11 @@ def do_help( return False + def do_dump(self, _signum: int, _frame: types.FrameType | None) -> None: + """ Signal handler to dump coverage report """ + print("Dumping coverage data ...") + self.coverage.save() + def do_run(self, options: optparse.Values, args: list[str]) -> int: """Implementation of 'coverage run'.""" @@ -851,6 +868,15 @@ def do_run(self, options: optparse.Values, args: list[str]) -> int: if options.append: self.coverage.load() + if options.dump_signal: + if options.dump_signal.upper() == 'USR1': + signal.signal(signal.SIGUSR1, self.do_dump) + elif options.dump_signal.upper() == 'USR2': + signal.signal(signal.SIGUSR2, self.do_dump) + else: + show_help(f"Unsupported signal for dump coverage report: {options.dump_signal}") + return ERR + # Run the script. self.coverage.start() code_ran = True diff --git a/doc/cmd.rst b/doc/cmd.rst index ee40a4fe5..23681cac8 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -137,13 +137,18 @@ There are many options: A list of directories or importable names of code to measure. --timid Use the slower Python trace function core. + --dump_signal=DUMP_SIGNAL + Define a system signal that will trigger coverage + report dump. It is important that target script do not + intercept this signal. Currently supported options + are: USR1, USR2. --debug=OPTS Debug options, separated by commas. [env: COVERAGE_DEBUG] -h, --help Get help on this command. --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: kxkJi2xQZv) If you want :ref:`branch coverage ` measurement, use the ``--branch`` flag. Otherwise only statement coverage is measured. @@ -215,6 +220,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: From cd2a9144d0fad1d6622ba3de57b9fa7c51310d38 Mon Sep 17 00:00:00 2001 From: Arkady Gilinsky <9481855+ark-g@users.noreply.github.com> Date: Sat, 12 Jul 2025 18:41:20 +0300 Subject: [PATCH 2/4] misc: Changes per pull request * Set options in alphabetical order * Rename "dump-signal" to "save-signal" Signed-off-by: Arkady Gilinsky <9481855+ark-g@users.noreply.github.com> --- coverage/cmdline.py | 40 ++++++++++++++++++++-------------------- doc/cmd.rst | 12 ++++++------ 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 480ced404..8b89d551a 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -190,6 +190,15 @@ 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." + ), + ) show_contexts = optparse.make_option( "--show-contexts", action="store_true", help="Show contexts for covered lines.", @@ -229,15 +238,6 @@ class Opts: "", "--version", action="store_true", help="Display version information and exit.", ) - dump_signal = optparse.make_option( - '', '--dump_signal', action='store', metavar='DUMP_SIGNAL', - choices = ['USR1', 'USR2'], - help=( - "Define a system signal that will trigger coverage report dump. " + - "It is important that target script do not intercept this signal. " + - "Currently supported options are: USR1, USR2." - ), - ) class CoverageOptionParser(optparse.OptionParser): """Base OptionParser for coverage.py. @@ -261,7 +261,6 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: data_file=None, debug=None, directory=None, - dump_signal=None, fail_under=None, format=None, help=None, @@ -275,6 +274,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, @@ -534,9 +534,9 @@ def get_prog_name(self) -> str: Opts.omit, Opts.pylib, Opts.parallel_mode, + Opts.save_signal, Opts.source, Opts.timid, - Opts.dump_signal, ] + GLOBAL_ARGS, usage="[options] [program options]", description="Run a Python program, measuring code execution.", @@ -819,9 +819,9 @@ def do_help( return False - def do_dump(self, _signum: int, _frame: types.FrameType | None) -> None: - """ Signal handler to dump coverage report """ - print("Dumping coverage data ...") + 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: @@ -868,13 +868,13 @@ def do_run(self, options: optparse.Values, args: list[str]) -> int: if options.append: self.coverage.load() - if options.dump_signal: - if options.dump_signal.upper() == 'USR1': - signal.signal(signal.SIGUSR1, self.do_dump) - elif options.dump_signal.upper() == 'USR2': - signal.signal(signal.SIGUSR2, self.do_dump) + if options.save_signal: + 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 dump coverage report: {options.dump_signal}") + show_help(f"Unsupported signal for save coverage report: {options.save_signal}") return ERR # Run the script. diff --git a/doc/cmd.rst b/doc/cmd.rst index 23681cac8..b1b99be86 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -133,22 +133,22 @@ 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. --source=SRC1,SRC2,... A list of directories or importable names of code to measure. --timid Use the slower Python trace function core. - --dump_signal=DUMP_SIGNAL - Define a system signal that will trigger coverage - report dump. It is important that target script do not - intercept this signal. Currently supported options - are: USR1, USR2. --debug=OPTS Debug options, separated by commas. [env: COVERAGE_DEBUG] -h, --help Get help on this command. --rcfile=RCFILE Specify configuration file. By default '.coveragerc', 'setup.cfg', 'tox.ini', and 'pyproject.toml' are tried. [env: COVERAGE_RCFILE] -.. [[[end]]] (sum: kxkJi2xQZv) +.. [[[end]]] (sum: 1+s3B5JO5I) If you want :ref:`branch coverage ` measurement, use the ``--branch`` flag. Otherwise only statement coverage is measured. From b4f34678bb8561d04f5a6e88cec38a28217fb199 Mon Sep 17 00:00:00 2001 From: Arkady Gilinsky <9481855+ark-g@users.noreply.github.com> Date: Sat, 12 Jul 2025 22:02:17 +0300 Subject: [PATCH 3/4] docs: Update - save signal doesn't work on Windows Signed-off-by: Arkady Gilinsky <9481855+ark-g@users.noreply.github.com> --- coverage/cmdline.py | 6 +++++- doc/cmd.rst | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 8b89d551a..115e6741e 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -196,7 +196,8 @@ class Opts: 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." + "Currently supported options are: USR1, USR2. " + + "This feature does not work on Windows." ), ) show_contexts = optparse.make_option( @@ -869,6 +870,9 @@ def do_run(self, options: optparse.Values, args: list[str]) -> int: 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': diff --git a/doc/cmd.rst b/doc/cmd.rst index b1b99be86..0446b1aa6 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -137,7 +137,8 @@ There are many options: 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. + 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. @@ -148,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: 1+s3B5JO5I) +.. [[[end]]] (sum: X8Kbvdq2+f) If you want :ref:`branch coverage ` measurement, use the ``--branch`` flag. Otherwise only statement coverage is measured. From 5d0acca2b8f8b745c8a5f7b82cb3f418ebae08e8 Mon Sep 17 00:00:00 2001 From: Arkady Gilinsky <9481855+ark-g@users.noreply.github.com> Date: Sun, 13 Jul 2025 20:24:57 +0300 Subject: [PATCH 4/4] test: Add test cases for signal dump Signed-off-by: Arkady Gilinsky <9481855+ark-g@users.noreply.github.com> --- tests/test_process.py | 45 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/test_process.py b/tests/test_process.py index 24eb1a68b..1856aa2e0 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -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")